@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 +22 -0
- package/dist/gobj-ui.cjs.js +191 -17
- package/dist/gobj-ui.es.js +191 -17
- package/package.json +1 -1
- package/src/c_yui_schema_editor.js +30 -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/src/host_drafts.js +68 -0
- package/src/host_drafts.test.js +62 -0
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
|
|
@@ -70139,6 +70250,44 @@ function register_c_yui_treedb_schema() {
|
|
|
70139
70250
|
return create_gclass$2(GCLASS_NAME$2);
|
|
70140
70251
|
}
|
|
70141
70252
|
//#endregion
|
|
70253
|
+
//#region src/host_drafts.js
|
|
70254
|
+
/***********************************************************************
|
|
70255
|
+
* host_drafts.js
|
|
70256
|
+
*
|
|
70257
|
+
* The topics a schema editor shows as DRAFTS (edited in
|
|
70258
|
+
* __system__, not yet saved), told by the host from data. The
|
|
70259
|
+
* editor marks a topic when a write of this session lands on
|
|
70260
|
+
* it, and that mark lived in the memory of the session only:
|
|
70261
|
+
* a reload of the page, a reconnect or a refresh of the model
|
|
70262
|
+
* lost the chip, the banner and the export warning while
|
|
70263
|
+
* __system__ still differed from the file in use (N13 of the
|
|
70264
|
+
* 2026-09-22 review). The host reads that difference from
|
|
70265
|
+
* C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
|
|
70266
|
+
* hands it here; the editor applies it to its model.
|
|
70267
|
+
*
|
|
70268
|
+
* Copyright (c) 2026, ArtGins.
|
|
70269
|
+
* All Rights Reserved.
|
|
70270
|
+
***********************************************************************/
|
|
70271
|
+
/************************************************************
|
|
70272
|
+
* Mark in `written` the topics of `model` the host names as
|
|
70273
|
+
* drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
|
|
70274
|
+
* topic the model does not hold is ignored. Returns how many were
|
|
70275
|
+
* marked.
|
|
70276
|
+
************************************************************/
|
|
70277
|
+
function mark_host_drafts(written, model, drafts) {
|
|
70278
|
+
let marked = 0;
|
|
70279
|
+
if (!written || !model || !Array.isArray(model.treedbs) || !drafts) return marked;
|
|
70280
|
+
for (let treedb of model.treedbs) {
|
|
70281
|
+
const names = drafts[treedb.id];
|
|
70282
|
+
if (!Array.isArray(names) || names.length === 0) continue;
|
|
70283
|
+
for (let topic of treedb.topics || []) if (names.includes(topic.name)) {
|
|
70284
|
+
written[topic.id] = true;
|
|
70285
|
+
marked++;
|
|
70286
|
+
}
|
|
70287
|
+
}
|
|
70288
|
+
return marked;
|
|
70289
|
+
}
|
|
70290
|
+
//#endregion
|
|
70142
70291
|
//#region src/schema_validate.js
|
|
70143
70292
|
/***********************************************************************
|
|
70144
70293
|
* schema_validate.js
|
|
@@ -71865,6 +72014,7 @@ function mt_create$1(gobj) {
|
|
|
71865
72014
|
let priv = gobj.priv;
|
|
71866
72015
|
priv.baseline = {};
|
|
71867
72016
|
priv.written = {};
|
|
72017
|
+
priv.host_drafts = {};
|
|
71868
72018
|
priv.save_queue = [];
|
|
71869
72019
|
build_ui$1(gobj);
|
|
71870
72020
|
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
@@ -72106,6 +72256,7 @@ function start_measuring(gobj) {
|
|
|
72106
72256
|
priv.written = {};
|
|
72107
72257
|
if (!priv.model) return;
|
|
72108
72258
|
for (let treedb of priv.model.treedbs) for (let topic of treedb.topics) priv.baseline[topic.id] = topic.topic_version;
|
|
72259
|
+
mark_host_drafts(priv.written, priv.model, priv.host_drafts);
|
|
72109
72260
|
}
|
|
72110
72261
|
/***************************************************************
|
|
72111
72262
|
* "This topic was written in this session and is not saved."
|
|
@@ -74543,6 +74694,23 @@ function ac_refresh$1(gobj, event, kw, src) {
|
|
|
74543
74694
|
return request_model(gobj);
|
|
74544
74695
|
}
|
|
74545
74696
|
/***************************************************************
|
|
74697
|
+
* {drafts: {treedb_name: [topic names]}} -- the host says which
|
|
74698
|
+
* topics hold a draft in __system__ that the file in use does not
|
|
74699
|
+
* have (C_TREEDB's saved-schema, `draft_changed`). The mark of a
|
|
74700
|
+
* write lived in this session's memory only, so a reload of the
|
|
74701
|
+
* page, a reconnect or a refresh showed no draft while __system__
|
|
74702
|
+
* still differed from the file (N13 of the 2026-09-22 review).
|
|
74703
|
+
* Replaces what the host said before; what this session wrote
|
|
74704
|
+
* stays marked.
|
|
74705
|
+
***************************************************************/
|
|
74706
|
+
function ac_drafts(gobj, event, kw, src) {
|
|
74707
|
+
let priv = gobj.priv;
|
|
74708
|
+
priv.host_drafts = kw && kw.drafts && typeof kw.drafts === "object" ? kw.drafts : {};
|
|
74709
|
+
mark_host_drafts(priv.written, priv.model, priv.host_drafts);
|
|
74710
|
+
if (priv.model) render(gobj);
|
|
74711
|
+
return 0;
|
|
74712
|
+
}
|
|
74713
|
+
/***************************************************************
|
|
74546
74714
|
* Moving about.
|
|
74547
74715
|
***************************************************************/
|
|
74548
74716
|
function ac_select_treedb(gobj, event, kw, src) {
|
|
@@ -75016,6 +75184,11 @@ function create_gclass$1(gclass_name) {
|
|
|
75016
75184
|
"EV_REFRESH",
|
|
75017
75185
|
ac_refresh$1,
|
|
75018
75186
|
null
|
|
75187
|
+
],
|
|
75188
|
+
[
|
|
75189
|
+
"EV_DRAFTS",
|
|
75190
|
+
ac_drafts,
|
|
75191
|
+
null
|
|
75019
75192
|
]
|
|
75020
75193
|
];
|
|
75021
75194
|
const TREEDB_WIDE = [
|
|
@@ -75164,6 +75337,7 @@ function create_gclass$1(gclass_name) {
|
|
|
75164
75337
|
["EV_TRANSPORT_STATE", 0],
|
|
75165
75338
|
["EV_LANGUAGE_CHANGED", 0],
|
|
75166
75339
|
["EV_REFRESH", 0],
|
|
75340
|
+
["EV_DRAFTS", 0],
|
|
75167
75341
|
["EV_SELECT_TREEDB", 0],
|
|
75168
75342
|
["EV_SELECT_TOPIC", 0],
|
|
75169
75343
|
["EV_BACK", 0],
|
package/package.json
CHANGED
|
@@ -113,6 +113,7 @@ import {
|
|
|
113
113
|
empty_string,
|
|
114
114
|
} from "@yuneta/gobj-js";
|
|
115
115
|
import {yui_tint} from "./bulma_tint.js";
|
|
116
|
+
import {mark_host_drafts} from "./host_drafts.js";
|
|
116
117
|
|
|
117
118
|
import "./c_yui_schema_editor.css";
|
|
118
119
|
|
|
@@ -248,6 +249,7 @@ function mt_create(gobj)
|
|
|
248
249
|
|
|
249
250
|
priv.baseline = {};
|
|
250
251
|
priv.written = {};
|
|
252
|
+
priv.host_drafts = {}; /* what the host says is a draft, {treedb: [topics]} (EV_DRAFTS) */
|
|
251
253
|
priv.save_queue = [];
|
|
252
254
|
|
|
253
255
|
build_ui(gobj);
|
|
@@ -571,6 +573,9 @@ function start_measuring(gobj)
|
|
|
571
573
|
priv.baseline[topic.id] = topic.topic_version;
|
|
572
574
|
}
|
|
573
575
|
}
|
|
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);
|
|
574
579
|
}
|
|
575
580
|
|
|
576
581
|
/***************************************************************
|
|
@@ -2755,6 +2760,28 @@ function ac_refresh(gobj, event, kw, src)
|
|
|
2755
2760
|
return request_model(gobj);
|
|
2756
2761
|
}
|
|
2757
2762
|
|
|
2763
|
+
/***************************************************************
|
|
2764
|
+
* {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.
|
|
2772
|
+
***************************************************************/
|
|
2773
|
+
function ac_drafts(gobj, event, kw, src)
|
|
2774
|
+
{
|
|
2775
|
+
let priv = gobj.priv;
|
|
2776
|
+
|
|
2777
|
+
priv.host_drafts = (kw && kw.drafts && typeof kw.drafts === "object")? kw.drafts: {};
|
|
2778
|
+
mark_host_drafts(priv.written, priv.model, priv.host_drafts);
|
|
2779
|
+
if(priv.model) {
|
|
2780
|
+
render(gobj);
|
|
2781
|
+
}
|
|
2782
|
+
return 0;
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2758
2785
|
/***************************************************************
|
|
2759
2786
|
* Moving about.
|
|
2760
2787
|
***************************************************************/
|
|
@@ -3328,7 +3355,8 @@ function create_gclass(gclass_name)
|
|
|
3328
3355
|
["EV_HIDE", ac_hide, null],
|
|
3329
3356
|
["EV_TRANSPORT_STATE", ac_transport_state, null],
|
|
3330
3357
|
["EV_LANGUAGE_CHANGED", ac_language_changed, null],
|
|
3331
|
-
["EV_REFRESH", ac_refresh, null]
|
|
3358
|
+
["EV_REFRESH", ac_refresh, null],
|
|
3359
|
+
["EV_DRAFTS", ac_drafts, null]
|
|
3332
3360
|
];
|
|
3333
3361
|
|
|
3334
3362
|
/* What a treedb is open for: reading it whole, and writing it
|
|
@@ -3411,6 +3439,7 @@ function create_gclass(gclass_name)
|
|
|
3411
3439
|
["EV_TRANSPORT_STATE", 0],
|
|
3412
3440
|
["EV_LANGUAGE_CHANGED", 0],
|
|
3413
3441
|
["EV_REFRESH", 0],
|
|
3442
|
+
["EV_DRAFTS", 0],
|
|
3414
3443
|
|
|
3415
3444
|
/* From this view's own screens. */
|
|
3416
3445
|
["EV_SELECT_TREEDB", 0],
|
|
@@ -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
|
+
};
|