@yuneta/gobj-ui 7.25.1 → 7.25.2
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 +6 -0
- package/dist/gobj-ui.cjs.js +128 -17
- package/dist/gobj-ui.es.js +128 -17
- package/package.json +1 -1
- package/src/c_yui_treedb_topic_with_form.js +10 -20
- package/src/c_yui_treedb_topics.js +59 -1
- package/src/form_busy.js +54 -0
- package/src/form_busy.test.js +84 -0
- package/src/form_writes_in_flight.js +60 -0
- package/src/form_writes_in_flight.test.js +45 -0
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
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
29426
|
-
* disabled, the save button spinning.
|
|
29427
|
-
*
|
|
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
|
-
|
|
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
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -23016,6 +23016,53 @@ function yui_shell_show_route_map(shell, opts) {
|
|
|
23016
23016
|
return modal_ref.modal;
|
|
23017
23017
|
}
|
|
23018
23018
|
//#endregion
|
|
23019
|
+
//#region src/form_writes_in_flight.js
|
|
23020
|
+
/***********************************************************************
|
|
23021
|
+
* form_writes_in_flight.js
|
|
23022
|
+
*
|
|
23023
|
+
* 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).
|
|
23031
|
+
*
|
|
23032
|
+
* Copyright (c) 2026, ArtGins.
|
|
23033
|
+
* All Rights Reserved.
|
|
23034
|
+
***********************************************************************/
|
|
23035
|
+
/************************************************************
|
|
23036
|
+
* A write left: remember whose it is
|
|
23037
|
+
************************************************************/
|
|
23038
|
+
function track_form_write(in_flight, form_write, topic_name) {
|
|
23039
|
+
if (!in_flight || !form_write) return;
|
|
23040
|
+
in_flight[form_write] = topic_name || "";
|
|
23041
|
+
}
|
|
23042
|
+
/************************************************************
|
|
23043
|
+
* A write was answered, one way or the other
|
|
23044
|
+
************************************************************/
|
|
23045
|
+
function settle_form_write(in_flight, form_write) {
|
|
23046
|
+
if (!in_flight || !form_write) return;
|
|
23047
|
+
delete in_flight[form_write];
|
|
23048
|
+
}
|
|
23049
|
+
/************************************************************
|
|
23050
|
+
* Nothing in flight can be answered any more: take them all,
|
|
23051
|
+
* as [{form_write, topic_name}], and leave the map empty.
|
|
23052
|
+
************************************************************/
|
|
23053
|
+
function abandon_form_writes(in_flight) {
|
|
23054
|
+
const abandoned = [];
|
|
23055
|
+
if (!in_flight) return abandoned;
|
|
23056
|
+
Object.keys(in_flight).forEach((serial) => {
|
|
23057
|
+
abandoned.push({
|
|
23058
|
+
form_write: Number(serial),
|
|
23059
|
+
topic_name: in_flight[serial]
|
|
23060
|
+
});
|
|
23061
|
+
delete in_flight[serial];
|
|
23062
|
+
});
|
|
23063
|
+
return abandoned;
|
|
23064
|
+
}
|
|
23065
|
+
//#endregion
|
|
23019
23066
|
//#region src/nodes_answer.js
|
|
23020
23067
|
/***********************************************************************
|
|
23021
23068
|
* nodes_answer.js
|
|
@@ -23582,6 +23629,7 @@ var __gclass__$6 = null;
|
|
|
23582
23629
|
* Framework Method: Create
|
|
23583
23630
|
***************************************************************/
|
|
23584
23631
|
function mt_create$6(gobj) {
|
|
23632
|
+
gobj.priv.form_writes = {};
|
|
23585
23633
|
build_ui$6(gobj);
|
|
23586
23634
|
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
23587
23635
|
if (!subscriber) subscriber = gobj_parent(gobj);
|
|
@@ -24738,6 +24786,7 @@ function treedb_update_node$1(gobj, treedb_name, topic_name, record, options, fo
|
|
|
24738
24786
|
log_error(ret);
|
|
24739
24787
|
return -1;
|
|
24740
24788
|
}
|
|
24789
|
+
track_form_write(gobj.priv.form_writes, form_write, topic_name);
|
|
24741
24790
|
return 0;
|
|
24742
24791
|
}
|
|
24743
24792
|
/************************************************************
|
|
@@ -25001,6 +25050,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
25001
25050
|
if (command === "update-node" || command === "create-node") {
|
|
25002
25051
|
let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
|
|
25003
25052
|
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));
|
|
25004
25054
|
answer_form_write(gobj, get_gobj_formtable(gobj, failed_topic), kw_get_int(gobj, kw_command, "form_write", 0, 0), false);
|
|
25005
25055
|
}
|
|
25006
25056
|
}
|
|
@@ -25038,6 +25088,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
25038
25088
|
created: command === "create-node",
|
|
25039
25089
|
command
|
|
25040
25090
|
});
|
|
25091
|
+
settle_form_write(gobj.priv.form_writes, kw_get_int(gobj, kw_command, "form_write", 0, 0));
|
|
25041
25092
|
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);
|
|
25042
25093
|
break;
|
|
25043
25094
|
case "delete-node": break;
|
|
@@ -25188,7 +25239,12 @@ function ac_back_to_topics(gobj, event, kw, src) {
|
|
|
25188
25239
|
* the session).
|
|
25189
25240
|
************************************************************/
|
|
25190
25241
|
function ac_transport_state$2(gobj, event, kw, src) {
|
|
25191
|
-
|
|
25242
|
+
let connected = !!(kw && kw.connected);
|
|
25243
|
+
refresh_toolbar_buttons(gobj, connected);
|
|
25244
|
+
if (!connected) abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
|
|
25245
|
+
log_warning(`${gobj_short_name(gobj)}: transport closed, the write ${w.form_write} of '${w.topic_name}' is lost`);
|
|
25246
|
+
answer_form_write(gobj, get_gobj_formtable(gobj, w.topic_name), w.form_write, false);
|
|
25247
|
+
});
|
|
25192
25248
|
return 0;
|
|
25193
25249
|
}
|
|
25194
25250
|
/************************************************************
|
|
@@ -25262,6 +25318,10 @@ function ac_create_record(gobj, event, kw, src) {
|
|
|
25262
25318
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
25263
25319
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
25264
25320
|
let record = kw.record;
|
|
25321
|
+
if (refuse_without_session(gobj, topic_name)) {
|
|
25322
|
+
answer_form_write(gobj, src, form_write, false);
|
|
25323
|
+
return -1;
|
|
25324
|
+
}
|
|
25265
25325
|
if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
|
|
25266
25326
|
list_dict: true,
|
|
25267
25327
|
create: true,
|
|
@@ -25429,6 +25489,10 @@ function ac_update_record(gobj, event, kw, src) {
|
|
|
25429
25489
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
25430
25490
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
25431
25491
|
let record = kw.record;
|
|
25492
|
+
if (refuse_without_session(gobj, topic_name)) {
|
|
25493
|
+
answer_form_write(gobj, src, form_write, false);
|
|
25494
|
+
return -1;
|
|
25495
|
+
}
|
|
25432
25496
|
if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
|
|
25433
25497
|
list_dict: true,
|
|
25434
25498
|
autolink: true
|
|
@@ -25439,6 +25503,18 @@ function ac_update_record(gobj, event, kw, src) {
|
|
|
25439
25503
|
return 0;
|
|
25440
25504
|
}
|
|
25441
25505
|
/************************************************************
|
|
25506
|
+
* A write sent with no session never leaves: C_IEVENT_CLI logs
|
|
25507
|
+
* "Not in session" and answers null, as it does when the command
|
|
25508
|
+
* went -- so a form waiting for the answer waited for ever (N8 of
|
|
25509
|
+
* the 2026-09-22 review). Refused before it is sent, aloud.
|
|
25510
|
+
************************************************************/
|
|
25511
|
+
function refuse_without_session(gobj, topic_name) {
|
|
25512
|
+
if (is_connected$1(gobj)) return false;
|
|
25513
|
+
log_error(`${gobj_short_name(gobj)}: no session, the write of '${topic_name}' was not sent`);
|
|
25514
|
+
yui_shell_show_error(yui_shell_of(gobj), "no session", { t });
|
|
25515
|
+
return true;
|
|
25516
|
+
}
|
|
25517
|
+
/************************************************************
|
|
25442
25518
|
* Tell a topic form how its write ended: EV_WRITE_DONE closes
|
|
25443
25519
|
* it, EV_WRITE_REFUSED leaves it open on what was typed. Every
|
|
25444
25520
|
* way a form's write can end passes here -- refused before it
|
|
@@ -26506,6 +26582,49 @@ function delete_impact(desc, records) {
|
|
|
26506
26582
|
return out;
|
|
26507
26583
|
}
|
|
26508
26584
|
//#endregion
|
|
26585
|
+
//#region src/form_busy.js
|
|
26586
|
+
/***********************************************************************
|
|
26587
|
+
* form_busy.js
|
|
26588
|
+
*
|
|
26589
|
+
* The toolbar of a topic form while a write is in flight: every
|
|
26590
|
+
* button disabled, the save button spinning. Bulma's `is-loading`
|
|
26591
|
+
* is the spinner; `disabled` is what stops the second click.
|
|
26592
|
+
*
|
|
26593
|
+
* A transition, not a setting: each button remembers whether it
|
|
26594
|
+
* was disabled BEFORE the toolbar went busy, and busy(false) puts
|
|
26595
|
+
* that back. So a second busy(true) while already busy must not
|
|
26596
|
+
* run at all -- it read the buttons it had just disabled as
|
|
26597
|
+
* "disabled on their own", and the busy(false) of a refused write
|
|
26598
|
+
* left Save and Cancel dead (N9 of the 2026-09-22 review: a record
|
|
26599
|
+
* with a picked file went busy for the read, then busy again for
|
|
26600
|
+
* the write). The caller owns `state` ({busy}) for the form's life.
|
|
26601
|
+
*
|
|
26602
|
+
* Copyright (c) 2026, ArtGins.
|
|
26603
|
+
* All Rights Reserved.
|
|
26604
|
+
***********************************************************************/
|
|
26605
|
+
/************************************************************
|
|
26606
|
+
* Move the toolbar to `busy`. Returns true when it moved, false when
|
|
26607
|
+
* it was already there (nothing touched).
|
|
26608
|
+
************************************************************/
|
|
26609
|
+
function set_toolbar_busy($container, state, busy) {
|
|
26610
|
+
busy = !!busy;
|
|
26611
|
+
if (!$container || !state) return false;
|
|
26612
|
+
if (!!state.busy === busy) return false;
|
|
26613
|
+
state.busy = busy;
|
|
26614
|
+
$container.querySelectorAll(".yui-toolbar-form button").forEach(($b) => {
|
|
26615
|
+
if (busy) {
|
|
26616
|
+
$b.dataset.was_disabled = $b.disabled ? "1" : "0";
|
|
26617
|
+
$b.disabled = true;
|
|
26618
|
+
} else if ($b.dataset.was_disabled !== void 0) {
|
|
26619
|
+
$b.disabled = $b.dataset.was_disabled === "1";
|
|
26620
|
+
delete $b.dataset.was_disabled;
|
|
26621
|
+
}
|
|
26622
|
+
});
|
|
26623
|
+
const $save = $container.querySelector(".yui-toolbar-form .button-save");
|
|
26624
|
+
if ($save) $save.classList.toggle("is-loading", busy);
|
|
26625
|
+
return true;
|
|
26626
|
+
}
|
|
26627
|
+
//#endregion
|
|
26509
26628
|
//#region src/table_cell_text.js
|
|
26510
26629
|
/****************************************************************************
|
|
26511
26630
|
* table_cell_text.js
|
|
@@ -26790,6 +26909,7 @@ function mt_create$5(gobj) {
|
|
|
26790
26909
|
gobj.priv.read_serial = 0;
|
|
26791
26910
|
gobj.priv.awaiting_write = 0;
|
|
26792
26911
|
gobj.priv.write_serial = 0;
|
|
26912
|
+
gobj.priv.form_busy = { busy: false };
|
|
26793
26913
|
let name = clean_name(gobj_name(gobj));
|
|
26794
26914
|
gobj_write_attr(gobj, "table_id", "table" + name);
|
|
26795
26915
|
gobj_write_attr(gobj, "toolbar_id", "toolbar" + name);
|
|
@@ -28193,6 +28313,7 @@ function teardown_form_child(gobj) {
|
|
|
28193
28313
|
let priv = gobj.priv;
|
|
28194
28314
|
priv.reading_files = 0;
|
|
28195
28315
|
priv.awaiting_write = 0;
|
|
28316
|
+
priv.form_busy = { busy: false };
|
|
28196
28317
|
if (priv.form) {
|
|
28197
28318
|
if (gobj_is_running(priv.form)) gobj_stop(priv.form);
|
|
28198
28319
|
gobj_destroy(priv.form);
|
|
@@ -29417,26 +29538,16 @@ function ac_form_save_record(gobj, event, kw, src) {
|
|
|
29417
29538
|
return 0;
|
|
29418
29539
|
}
|
|
29419
29540
|
/************************************************************
|
|
29420
|
-
* The form's toolbar while a read is in flight: every
|
|
29421
|
-
* disabled, the save button spinning.
|
|
29422
|
-
*
|
|
29541
|
+
* The form's toolbar while a read or a write is in flight: every
|
|
29542
|
+
* button disabled, the save button spinning (set_toolbar_busy). A
|
|
29543
|
+
* transition: busy(true) while busy touches nothing, or a record
|
|
29544
|
+
* with a picked file -- busy for the read, busy again for the
|
|
29545
|
+
* write -- came back from a refusal with Save and Cancel dead.
|
|
29423
29546
|
************************************************************/
|
|
29424
29547
|
function set_form_busy(gobj, busy) {
|
|
29425
29548
|
let priv = gobj.priv;
|
|
29426
29549
|
if (!priv.form) return;
|
|
29427
|
-
|
|
29428
|
-
if (!$container) return;
|
|
29429
|
-
$container.querySelectorAll(".yui-toolbar-form button").forEach(($b) => {
|
|
29430
|
-
if (busy) {
|
|
29431
|
-
$b.dataset.was_disabled = $b.disabled ? "1" : "0";
|
|
29432
|
-
$b.disabled = true;
|
|
29433
|
-
} else if ($b.dataset.was_disabled !== void 0) {
|
|
29434
|
-
$b.disabled = $b.dataset.was_disabled === "1";
|
|
29435
|
-
delete $b.dataset.was_disabled;
|
|
29436
|
-
}
|
|
29437
|
-
});
|
|
29438
|
-
const $save = $container.querySelector(".yui-toolbar-form .button-save");
|
|
29439
|
-
if ($save) $save.classList.toggle("is-loading", !!busy);
|
|
29550
|
+
set_toolbar_busy(gobj_read_attr(priv.form, "$container"), priv.form_busy, busy);
|
|
29440
29551
|
}
|
|
29441
29552
|
/************************************************************
|
|
29442
29553
|
* Is this the read the view is waiting for? A read whose form was
|
package/package.json
CHANGED
|
@@ -110,6 +110,7 @@ import {
|
|
|
110
110
|
import {delete_impact} from "./delete_impact.js";
|
|
111
111
|
|
|
112
112
|
import "./c_yui_treedb_topic_with_form.css";
|
|
113
|
+
import {set_toolbar_busy} from "./form_busy.js";
|
|
113
114
|
import "./tabulator.css";
|
|
114
115
|
import {cell_text, hook_cell_spec} from "./table_cell_text.js";
|
|
115
116
|
|
|
@@ -432,6 +433,8 @@ function mt_create(gobj)
|
|
|
432
433
|
* serial the host echoes in EV_WRITE_DONE / EV_WRITE_REFUSED. */
|
|
433
434
|
gobj.priv.awaiting_write = 0;
|
|
434
435
|
gobj.priv.write_serial = 0;
|
|
436
|
+
/* The busy transition of the form's toolbar (set_toolbar_busy) */
|
|
437
|
+
gobj.priv.form_busy = {busy: false};
|
|
435
438
|
|
|
436
439
|
let name = clean_name(gobj_name(gobj));
|
|
437
440
|
gobj_write_attr(gobj, "table_id", "table" + name);
|
|
@@ -2503,6 +2506,7 @@ function teardown_form_child(gobj)
|
|
|
2503
2506
|
let priv = gobj.priv;
|
|
2504
2507
|
priv.reading_files = 0; // a read in flight lands on nobody
|
|
2505
2508
|
priv.awaiting_write = 0; // and so does the answer of a write
|
|
2509
|
+
priv.form_busy = {busy: false}; // its toolbar goes with it
|
|
2506
2510
|
if(priv.form) {
|
|
2507
2511
|
if(gobj_is_running(priv.form)) {
|
|
2508
2512
|
gobj_stop(priv.form);
|
|
@@ -4261,9 +4265,11 @@ function ac_form_save_record(gobj, event, kw, src)
|
|
|
4261
4265
|
}
|
|
4262
4266
|
|
|
4263
4267
|
/************************************************************
|
|
4264
|
-
* The form's toolbar while a read is in flight: every
|
|
4265
|
-
* disabled, the save button spinning.
|
|
4266
|
-
*
|
|
4268
|
+
* The form's toolbar while a read or a write is in flight: every
|
|
4269
|
+
* button disabled, the save button spinning (set_toolbar_busy). A
|
|
4270
|
+
* transition: busy(true) while busy touches nothing, or a record
|
|
4271
|
+
* with a picked file -- busy for the read, busy again for the
|
|
4272
|
+
* write -- came back from a refusal with Save and Cancel dead.
|
|
4267
4273
|
************************************************************/
|
|
4268
4274
|
function set_form_busy(gobj, busy)
|
|
4269
4275
|
{
|
|
@@ -4271,23 +4277,7 @@ function set_form_busy(gobj, busy)
|
|
|
4271
4277
|
if(!priv.form) {
|
|
4272
4278
|
return;
|
|
4273
4279
|
}
|
|
4274
|
-
|
|
4275
|
-
if(!$container) {
|
|
4276
|
-
return;
|
|
4277
|
-
}
|
|
4278
|
-
$container.querySelectorAll('.yui-toolbar-form button').forEach(($b) => {
|
|
4279
|
-
if(busy) {
|
|
4280
|
-
$b.dataset.was_disabled = $b.disabled? "1": "0";
|
|
4281
|
-
$b.disabled = true;
|
|
4282
|
-
} else if($b.dataset.was_disabled !== undefined) {
|
|
4283
|
-
$b.disabled = ($b.dataset.was_disabled === "1");
|
|
4284
|
-
delete $b.dataset.was_disabled;
|
|
4285
|
-
}
|
|
4286
|
-
});
|
|
4287
|
-
const $save = $container.querySelector('.yui-toolbar-form .button-save');
|
|
4288
|
-
if($save) {
|
|
4289
|
-
$save.classList.toggle('is-loading', !!busy);
|
|
4290
|
-
}
|
|
4280
|
+
set_toolbar_busy(gobj_read_attr(priv.form, "$container"), priv.form_busy, busy);
|
|
4291
4281
|
}
|
|
4292
4282
|
|
|
4293
4283
|
/************************************************************
|
|
@@ -57,6 +57,11 @@ import {
|
|
|
57
57
|
} from "@yuneta/gobj-js";
|
|
58
58
|
|
|
59
59
|
import "./c_yui_treedb_topics.css";
|
|
60
|
+
import {
|
|
61
|
+
track_form_write,
|
|
62
|
+
settle_form_write,
|
|
63
|
+
abandon_form_writes,
|
|
64
|
+
} from "./form_writes_in_flight.js";
|
|
60
65
|
|
|
61
66
|
import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
|
|
62
67
|
import {
|
|
@@ -151,6 +156,11 @@ let __gclass__ = null;
|
|
|
151
156
|
***************************************************************/
|
|
152
157
|
function mt_create(gobj)
|
|
153
158
|
{
|
|
159
|
+
/* The writes of topic forms sent and not yet answered, by serial
|
|
160
|
+
* (form_writes_in_flight): answered refused when the transport
|
|
161
|
+
* closes under them. */
|
|
162
|
+
gobj.priv.form_writes = {};
|
|
163
|
+
|
|
154
164
|
build_ui(gobj);
|
|
155
165
|
|
|
156
166
|
/*
|
|
@@ -1655,6 +1665,7 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options, form
|
|
|
1655
1665
|
log_error(ret);
|
|
1656
1666
|
return -1;
|
|
1657
1667
|
}
|
|
1668
|
+
track_form_write(gobj.priv.form_writes, form_write, topic_name);
|
|
1658
1669
|
return 0;
|
|
1659
1670
|
}
|
|
1660
1671
|
|
|
@@ -2065,6 +2076,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2065
2076
|
gobj_send_event(gobj, "EV_REFRESH_TOPIC",
|
|
2066
2077
|
{topic_name: failed_topic}, gobj);
|
|
2067
2078
|
}
|
|
2079
|
+
settle_form_write(gobj.priv.form_writes,
|
|
2080
|
+
kw_get_int(gobj, kw_command, "form_write", 0, 0));
|
|
2068
2081
|
answer_form_write(
|
|
2069
2082
|
gobj,
|
|
2070
2083
|
get_gobj_formtable(gobj, failed_topic),
|
|
@@ -2163,6 +2176,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2163
2176
|
created: (command === "create-node"),
|
|
2164
2177
|
command: command
|
|
2165
2178
|
});
|
|
2179
|
+
settle_form_write(gobj.priv.form_writes,
|
|
2180
|
+
kw_get_int(gobj, kw_command, "form_write", 0, 0));
|
|
2166
2181
|
answer_form_write(
|
|
2167
2182
|
gobj,
|
|
2168
2183
|
get_gobj_formtable(gobj, kw_get_str(gobj, kw_command, "topic_name", "", 0)),
|
|
@@ -2406,7 +2421,26 @@ function ac_back_to_topics(gobj, event, kw, src)
|
|
|
2406
2421
|
************************************************************/
|
|
2407
2422
|
function ac_transport_state(gobj, event, kw, src)
|
|
2408
2423
|
{
|
|
2409
|
-
|
|
2424
|
+
let connected = !!(kw && kw.connected);
|
|
2425
|
+
refresh_toolbar_buttons(gobj, connected);
|
|
2426
|
+
|
|
2427
|
+
/*
|
|
2428
|
+
* A write in flight when the transport closes is never answered by
|
|
2429
|
+
* the backend: its form stayed open and busy for ever (N8 of the
|
|
2430
|
+
* 2026-09-22 review). Answered refused: the form stays open on what
|
|
2431
|
+
* was typed, and the person saves again when the session is back.
|
|
2432
|
+
*/
|
|
2433
|
+
if(!connected) {
|
|
2434
|
+
abandon_form_writes(gobj.priv.form_writes).forEach((w) => {
|
|
2435
|
+
log_warning(`${gobj_short_name(gobj)}: transport closed, the write ${w.form_write} of '${w.topic_name}' is lost`);
|
|
2436
|
+
answer_form_write(
|
|
2437
|
+
gobj,
|
|
2438
|
+
get_gobj_formtable(gobj, w.topic_name),
|
|
2439
|
+
w.form_write,
|
|
2440
|
+
false
|
|
2441
|
+
);
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2410
2444
|
return 0;
|
|
2411
2445
|
}
|
|
2412
2446
|
|
|
@@ -2522,6 +2556,10 @@ function ac_create_record(gobj, event, kw, src)
|
|
|
2522
2556
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
2523
2557
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
2524
2558
|
let record = kw.record;
|
|
2559
|
+
if(refuse_without_session(gobj, topic_name)) {
|
|
2560
|
+
answer_form_write(gobj, src, form_write, false);
|
|
2561
|
+
return -1; /* Error already logged */
|
|
2562
|
+
}
|
|
2525
2563
|
|
|
2526
2564
|
/* `create_only`: +New makes a NEW record. With `create` alone a taken
|
|
2527
2565
|
* id was an update -- the record overwritten and, through autolink
|
|
@@ -2751,6 +2789,10 @@ function ac_update_record(gobj, event, kw, src)
|
|
|
2751
2789
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
2752
2790
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
2753
2791
|
let record = kw.record;
|
|
2792
|
+
if(refuse_without_session(gobj, topic_name)) {
|
|
2793
|
+
answer_form_write(gobj, src, form_write, false);
|
|
2794
|
+
return -1; /* Error already logged */
|
|
2795
|
+
}
|
|
2754
2796
|
|
|
2755
2797
|
let options = {
|
|
2756
2798
|
list_dict: true,
|
|
@@ -2765,6 +2807,22 @@ function ac_update_record(gobj, event, kw, src)
|
|
|
2765
2807
|
return 0;
|
|
2766
2808
|
}
|
|
2767
2809
|
|
|
2810
|
+
/************************************************************
|
|
2811
|
+
* A write sent with no session never leaves: C_IEVENT_CLI logs
|
|
2812
|
+
* "Not in session" and answers null, as it does when the command
|
|
2813
|
+
* went -- so a form waiting for the answer waited for ever (N8 of
|
|
2814
|
+
* the 2026-09-22 review). Refused before it is sent, aloud.
|
|
2815
|
+
************************************************************/
|
|
2816
|
+
function refuse_without_session(gobj, topic_name)
|
|
2817
|
+
{
|
|
2818
|
+
if(is_connected(gobj)) {
|
|
2819
|
+
return false;
|
|
2820
|
+
}
|
|
2821
|
+
log_error(`${gobj_short_name(gobj)}: no session, the write of '${topic_name}' was not sent`);
|
|
2822
|
+
yui_shell_show_error(yui_shell_of(gobj), "no session", {t: t});
|
|
2823
|
+
return true;
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2768
2826
|
/************************************************************
|
|
2769
2827
|
* Tell a topic form how its write ended: EV_WRITE_DONE closes
|
|
2770
2828
|
* it, EV_WRITE_REFUSED leaves it open on what was typed. Every
|
package/src/form_busy.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* form_busy.js
|
|
3
|
+
*
|
|
4
|
+
* The toolbar of a topic form while a write is in flight: every
|
|
5
|
+
* button disabled, the save button spinning. Bulma's `is-loading`
|
|
6
|
+
* is the spinner; `disabled` is what stops the second click.
|
|
7
|
+
*
|
|
8
|
+
* A transition, not a setting: each button remembers whether it
|
|
9
|
+
* was disabled BEFORE the toolbar went busy, and busy(false) puts
|
|
10
|
+
* that back. So a second busy(true) while already busy must not
|
|
11
|
+
* run at all -- it read the buttons it had just disabled as
|
|
12
|
+
* "disabled on their own", and the busy(false) of a refused write
|
|
13
|
+
* left Save and Cancel dead (N9 of the 2026-09-22 review: a record
|
|
14
|
+
* with a picked file went busy for the read, then busy again for
|
|
15
|
+
* the write). The caller owns `state` ({busy}) for the form's life.
|
|
16
|
+
*
|
|
17
|
+
* Copyright (c) 2026, ArtGins.
|
|
18
|
+
* All Rights Reserved.
|
|
19
|
+
***********************************************************************/
|
|
20
|
+
|
|
21
|
+
/************************************************************
|
|
22
|
+
* Move the toolbar to `busy`. Returns true when it moved, false when
|
|
23
|
+
* it was already there (nothing touched).
|
|
24
|
+
************************************************************/
|
|
25
|
+
function set_toolbar_busy($container, state, busy)
|
|
26
|
+
{
|
|
27
|
+
busy = !!busy;
|
|
28
|
+
if(!$container || !state) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if(!!state.busy === busy) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
state.busy = busy;
|
|
35
|
+
|
|
36
|
+
$container.querySelectorAll('.yui-toolbar-form button').forEach(($b) => {
|
|
37
|
+
if(busy) {
|
|
38
|
+
$b.dataset.was_disabled = $b.disabled? "1": "0";
|
|
39
|
+
$b.disabled = true;
|
|
40
|
+
} else if($b.dataset.was_disabled !== undefined) {
|
|
41
|
+
$b.disabled = ($b.dataset.was_disabled === "1");
|
|
42
|
+
delete $b.dataset.was_disabled;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const $save = $container.querySelector('.yui-toolbar-form .button-save');
|
|
46
|
+
if($save) {
|
|
47
|
+
$save.classList.toggle('is-loading', busy);
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
set_toolbar_busy,
|
|
54
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
import {set_toolbar_busy} from "./form_busy.js";
|
|
3
|
+
|
|
4
|
+
/* A toolbar double: the two things the helper touches on a button are
|
|
5
|
+
* `disabled` and `dataset`, and on the save button its class list. */
|
|
6
|
+
function button(cls, disabled)
|
|
7
|
+
{
|
|
8
|
+
const classes = new Set(cls.split(" "));
|
|
9
|
+
return {
|
|
10
|
+
disabled: disabled,
|
|
11
|
+
dataset: {},
|
|
12
|
+
classList: {
|
|
13
|
+
toggle: (c, on) => {
|
|
14
|
+
if(on) {
|
|
15
|
+
classes.add(c);
|
|
16
|
+
} else {
|
|
17
|
+
classes.delete(c);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
contains: (c) => classes.has(c),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toolbar()
|
|
26
|
+
{
|
|
27
|
+
const save = button("button-save", false);
|
|
28
|
+
const cancel = button("button-cancel", false);
|
|
29
|
+
const undo = button("button-undo", true); /* disabled on its own */
|
|
30
|
+
const buttons = [save, cancel, undo];
|
|
31
|
+
return {
|
|
32
|
+
save, cancel, undo,
|
|
33
|
+
$container: {
|
|
34
|
+
querySelectorAll: () => buttons,
|
|
35
|
+
querySelector: () => save,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("set_toolbar_busy", () => {
|
|
41
|
+
|
|
42
|
+
it("busy then free: every button is as it was", () => {
|
|
43
|
+
const tb = toolbar();
|
|
44
|
+
const state = {busy: false};
|
|
45
|
+
expect(set_toolbar_busy(tb.$container, state, true)).toBe(true);
|
|
46
|
+
expect(tb.save.disabled).toBe(true);
|
|
47
|
+
expect(tb.cancel.disabled).toBe(true);
|
|
48
|
+
expect(tb.save.classList.contains("is-loading")).toBe(true);
|
|
49
|
+
expect(set_toolbar_busy(tb.$container, state, false)).toBe(true);
|
|
50
|
+
expect(tb.save.disabled).toBe(false);
|
|
51
|
+
expect(tb.cancel.disabled).toBe(false);
|
|
52
|
+
expect(tb.undo.disabled).toBe(true);
|
|
53
|
+
expect(tb.save.classList.contains("is-loading")).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("busy twice then free: Save and Cancel come back (N9)", () => {
|
|
57
|
+
/* A record with a picked file went busy for the read and busy
|
|
58
|
+
* again for the write; the second pass recorded the buttons the
|
|
59
|
+
* first had disabled as disabled on their own, and the free of a
|
|
60
|
+
* refused write left Save and Cancel dead. */
|
|
61
|
+
const tb = toolbar();
|
|
62
|
+
const state = {busy: false};
|
|
63
|
+
set_toolbar_busy(tb.$container, state, true);
|
|
64
|
+
expect(set_toolbar_busy(tb.$container, state, true)).toBe(false);
|
|
65
|
+
set_toolbar_busy(tb.$container, state, false);
|
|
66
|
+
expect(tb.save.disabled).toBe(false);
|
|
67
|
+
expect(tb.cancel.disabled).toBe(false);
|
|
68
|
+
expect(tb.undo.disabled).toBe(true);
|
|
69
|
+
expect(state.busy).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("free while free touches nothing", () => {
|
|
73
|
+
const tb = toolbar();
|
|
74
|
+
const state = {busy: false};
|
|
75
|
+
expect(set_toolbar_busy(tb.$container, state, false)).toBe(false);
|
|
76
|
+
expect(tb.save.disabled).toBe(false);
|
|
77
|
+
expect(tb.undo.disabled).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("no container, no state: nothing, and says so", () => {
|
|
81
|
+
expect(set_toolbar_busy(null, {busy: false}, true)).toBe(false);
|
|
82
|
+
expect(set_toolbar_busy(toolbar().$container, null, true)).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* form_writes_in_flight.js
|
|
3
|
+
*
|
|
4
|
+
* The writes of topic forms a C_YUI_TREEDB_TOPICS view has sent
|
|
5
|
+
* and not yet answered, by their `form_write` serial. A form that
|
|
6
|
+
* waits for its answer (form_waits_for_answer) stays open and
|
|
7
|
+
* busy until the host answers, so the host has to answer EVERY
|
|
8
|
+
* way a write ends -- and the transport closing under a write in
|
|
9
|
+
* flight, or a command that never left because there was no
|
|
10
|
+
* session, are two of them that nobody answered: the form stayed
|
|
11
|
+
* busy for ever (N8 of the 2026-09-22 review).
|
|
12
|
+
*
|
|
13
|
+
* Copyright (c) 2026, ArtGins.
|
|
14
|
+
* All Rights Reserved.
|
|
15
|
+
***********************************************************************/
|
|
16
|
+
|
|
17
|
+
/************************************************************
|
|
18
|
+
* A write left: remember whose it is
|
|
19
|
+
************************************************************/
|
|
20
|
+
function track_form_write(in_flight, form_write, topic_name)
|
|
21
|
+
{
|
|
22
|
+
if(!in_flight || !form_write) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
in_flight[form_write] = topic_name || "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/************************************************************
|
|
29
|
+
* A write was answered, one way or the other
|
|
30
|
+
************************************************************/
|
|
31
|
+
function settle_form_write(in_flight, form_write)
|
|
32
|
+
{
|
|
33
|
+
if(!in_flight || !form_write) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
delete in_flight[form_write];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/************************************************************
|
|
40
|
+
* Nothing in flight can be answered any more: take them all,
|
|
41
|
+
* as [{form_write, topic_name}], and leave the map empty.
|
|
42
|
+
************************************************************/
|
|
43
|
+
function abandon_form_writes(in_flight)
|
|
44
|
+
{
|
|
45
|
+
const abandoned = [];
|
|
46
|
+
if(!in_flight) {
|
|
47
|
+
return abandoned;
|
|
48
|
+
}
|
|
49
|
+
Object.keys(in_flight).forEach((serial) => {
|
|
50
|
+
abandoned.push({form_write: Number(serial), topic_name: in_flight[serial]});
|
|
51
|
+
delete in_flight[serial];
|
|
52
|
+
});
|
|
53
|
+
return abandoned;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
track_form_write,
|
|
58
|
+
settle_form_write,
|
|
59
|
+
abandon_form_writes,
|
|
60
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
track_form_write,
|
|
4
|
+
settle_form_write,
|
|
5
|
+
abandon_form_writes,
|
|
6
|
+
} from "./form_writes_in_flight.js";
|
|
7
|
+
|
|
8
|
+
describe("form writes in flight", () => {
|
|
9
|
+
|
|
10
|
+
it("a write answered is settled; the others stay", () => {
|
|
11
|
+
const in_flight = {};
|
|
12
|
+
track_form_write(in_flight, 1, "users");
|
|
13
|
+
track_form_write(in_flight, 2, "roles");
|
|
14
|
+
settle_form_write(in_flight, 1);
|
|
15
|
+
expect(Object.keys(in_flight)).toEqual(["2"]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("the transport closes: every write in flight is abandoned, once (N8)", () => {
|
|
19
|
+
/* The form of each one is answered EV_WRITE_REFUSED by the host
|
|
20
|
+
* and stays open on what was typed; before, it stayed busy for
|
|
21
|
+
* ever, because nobody answered a write whose answer was lost. */
|
|
22
|
+
const in_flight = {};
|
|
23
|
+
track_form_write(in_flight, 3, "users");
|
|
24
|
+
track_form_write(in_flight, 4, "roles");
|
|
25
|
+
const abandoned = abandon_form_writes(in_flight);
|
|
26
|
+
expect(abandoned).toEqual([
|
|
27
|
+
{form_write: 3, topic_name: "users"},
|
|
28
|
+
{form_write: 4, topic_name: "roles"},
|
|
29
|
+
]);
|
|
30
|
+
expect(abandon_form_writes(in_flight)).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("a cell edited in place (no serial) is not a form write", () => {
|
|
34
|
+
const in_flight = {};
|
|
35
|
+
track_form_write(in_flight, 0, "users");
|
|
36
|
+
track_form_write(in_flight, undefined, "users");
|
|
37
|
+
expect(abandon_form_writes(in_flight)).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("no map: nothing to do", () => {
|
|
41
|
+
expect(() => track_form_write(null, 1, "x")).not.toThrow();
|
|
42
|
+
expect(() => settle_form_write(null, 1)).not.toThrow();
|
|
43
|
+
expect(abandon_form_writes(null)).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
});
|