@yuneta/gobj-ui 7.25.3 → 7.25.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -1
- package/dist/gobj-ui.cjs.js +205 -48
- package/dist/gobj-ui.es.js +206 -49
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +12 -0
- package/src/c_yui_schema_editor.js +42 -23
- package/src/c_yui_shell.js +61 -8
- package/src/c_yui_treedb_topic_with_form.js +20 -1
- package/src/c_yui_treedb_topics.js +41 -10
- package/src/form_time_value.js +23 -0
- package/src/form_writes_in_flight.js +32 -13
- package/src/form_writes_in_flight.test.js +18 -3
- package/src/host_drafts.js +28 -2
- package/src/host_drafts.test.js +24 -1
- package/src/schema_editor_drafts.wiring.test.js +176 -0
- package/src/schema_version_bump.test.js +10 -5
- package/src/treedb_topics_transport.wiring.test.js +228 -0
- package/src/treedb_write_plan.js +32 -1
- package/src/treedb_write_plan.test.js +22 -1
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
* the owner's design of M36 (2026-09-21 review) every write here
|
|
27
27
|
* raised both versions, so an edit half made was already the
|
|
28
28
|
* schema of the next start. The topic list marks what this session
|
|
29
|
-
* wrote and has not saved
|
|
30
|
-
* what forgets it
|
|
29
|
+
* wrote and has not saved (the host's EV_REFRESH after a save is
|
|
30
|
+
* what forgets it) and what the host says is not saved (EV_DRAFTS,
|
|
31
|
+
* replaced whole each time it is sent).
|
|
31
32
|
*
|
|
32
33
|
* WHAT ELSE IS HERE, and why each is here and not somewhere else:
|
|
33
34
|
*
|
|
@@ -113,7 +114,7 @@ import {
|
|
|
113
114
|
empty_string,
|
|
114
115
|
} from "@yuneta/gobj-js";
|
|
115
116
|
import {yui_tint} from "./bulma_tint.js";
|
|
116
|
-
import {
|
|
117
|
+
import {host_draft_ids} from "./host_drafts.js";
|
|
117
118
|
|
|
118
119
|
import "./c_yui_schema_editor.css";
|
|
119
120
|
|
|
@@ -207,6 +208,8 @@ let PRIVATE_DATA = {
|
|
|
207
208
|
* version still has to move. */
|
|
208
209
|
baseline: null, /* {topic id: topic_version} at load time */
|
|
209
210
|
written: null, /* {topic id: true} */
|
|
211
|
+
host_drafts: null, /* {treedb: [topic names]}, the host's last EV_DRAFTS */
|
|
212
|
+
host_ids: null, /* host_drafts resolved on the model, {topic id: true} */
|
|
210
213
|
order_undo: null, /* {topic id: [{id, order}]} before the first drag */
|
|
211
214
|
|
|
212
215
|
/* Children and overlays */
|
|
@@ -250,6 +253,7 @@ function mt_create(gobj)
|
|
|
250
253
|
priv.baseline = {};
|
|
251
254
|
priv.written = {};
|
|
252
255
|
priv.host_drafts = {}; /* what the host says is a draft, {treedb: [topics]} (EV_DRAFTS) */
|
|
256
|
+
priv.host_ids = {}; /* the same, as {topic id: true} of the model */
|
|
253
257
|
priv.save_queue = [];
|
|
254
258
|
|
|
255
259
|
build_ui(gobj);
|
|
@@ -564,6 +568,7 @@ function start_measuring(gobj)
|
|
|
564
568
|
|
|
565
569
|
priv.baseline = {};
|
|
566
570
|
priv.written = {};
|
|
571
|
+
priv.host_ids = {};
|
|
567
572
|
|
|
568
573
|
if(!priv.model) {
|
|
569
574
|
return;
|
|
@@ -573,29 +578,39 @@ function start_measuring(gobj)
|
|
|
573
578
|
priv.baseline[topic.id] = topic.topic_version;
|
|
574
579
|
}
|
|
575
580
|
}
|
|
576
|
-
/* A draft is a draft after a reload too: what the host said
|
|
577
|
-
*
|
|
578
|
-
|
|
581
|
+
/* A draft is a draft after a reload too: what the host said last
|
|
582
|
+
* is resolved against the new model (host_drafts.js), apart from
|
|
583
|
+
* what this session writes, until the host says otherwise. */
|
|
584
|
+
priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
|
|
579
585
|
}
|
|
580
586
|
|
|
581
587
|
/***************************************************************
|
|
582
|
-
* "This topic
|
|
588
|
+
* "This topic holds an edit that is not saved."
|
|
583
589
|
*
|
|
584
590
|
* An edit is a DRAFT: it moves no version and reaches no treedb
|
|
585
591
|
* until the host saves it (C_TREEDB's save-schema, which raises the
|
|
586
|
-
* versions of what changed)
|
|
587
|
-
*
|
|
592
|
+
* versions of what changed). Two sources say a topic is one, and
|
|
593
|
+
* they are kept APART because they are forgotten differently:
|
|
594
|
+
* - `written`: what THIS session wrote since the model was loaded
|
|
595
|
+
* (a reload, the host's EV_REFRESH, forgets it -- the store says
|
|
596
|
+
* what is left);
|
|
597
|
+
* - `host_ids`: what the host said last (EV_DRAFTS), replaced WHOLE
|
|
598
|
+
* by the next EV_DRAFTS. Folded into `written`, a topic the host
|
|
599
|
+
* named once stayed a draft after the Save that published it.
|
|
588
600
|
*
|
|
589
601
|
* ONE predicate, because it is asked in three places -- the topic
|
|
590
602
|
* list draws a chip with it, the column view a banner, the export
|
|
591
603
|
* a warning -- and copies of a rule are how they drift.
|
|
592
604
|
***************************************************************/
|
|
593
|
-
function topic_is_draft(
|
|
605
|
+
function topic_is_draft(priv, topic)
|
|
594
606
|
{
|
|
595
|
-
if(!topic || !
|
|
607
|
+
if(!topic || !priv) {
|
|
596
608
|
return false;
|
|
597
609
|
}
|
|
598
|
-
|
|
610
|
+
if(priv.written && priv.written[topic.id]) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
return (priv.host_ids && priv.host_ids[topic.id])? true: false;
|
|
599
614
|
}
|
|
600
615
|
|
|
601
616
|
/***************************************************************
|
|
@@ -1050,7 +1065,7 @@ function render_topics(gobj, $body)
|
|
|
1050
1065
|
let twice = repeated_names(treedb.topics);
|
|
1051
1066
|
let $rows = [];
|
|
1052
1067
|
for(let topic of treedb.topics) {
|
|
1053
|
-
let pending = topic_is_draft(priv
|
|
1068
|
+
let pending = topic_is_draft(priv, topic);
|
|
1054
1069
|
/* Two topics of one name: the id is what tells them apart, and
|
|
1055
1070
|
* it is what the url has to carry — addressing by name would
|
|
1056
1071
|
* open the first one whichever row was clicked. */
|
|
@@ -1256,7 +1271,7 @@ function draft_banner(gobj, topic)
|
|
|
1256
1271
|
{
|
|
1257
1272
|
let priv = gobj.priv;
|
|
1258
1273
|
|
|
1259
|
-
if(!topic_is_draft(priv
|
|
1274
|
+
if(!topic_is_draft(priv, topic)) {
|
|
1260
1275
|
return null;
|
|
1261
1276
|
}
|
|
1262
1277
|
if(is_readonly(gobj)) {
|
|
@@ -1997,7 +2012,7 @@ function open_report(gobj, findings, treedb_id)
|
|
|
1997
2012
|
function open_export(gobj, treedb)
|
|
1998
2013
|
{
|
|
1999
2014
|
let priv = gobj.priv;
|
|
2000
|
-
let drafts = treedb.topics.some((topic) => topic_is_draft(priv
|
|
2015
|
+
let drafts = treedb.topics.some((topic) => topic_is_draft(priv, topic));
|
|
2001
2016
|
let c_text = schema_to_c(treedb);
|
|
2002
2017
|
let json_text = JSON.stringify(schema_to_json(treedb), null, 4);
|
|
2003
2018
|
|
|
@@ -2762,20 +2777,24 @@ function ac_refresh(gobj, event, kw, src)
|
|
|
2762
2777
|
|
|
2763
2778
|
/***************************************************************
|
|
2764
2779
|
* {drafts: {treedb_name: [topic names]}} -- the host says which
|
|
2765
|
-
* topics hold a draft in __system__ that
|
|
2766
|
-
*
|
|
2767
|
-
*
|
|
2768
|
-
*
|
|
2769
|
-
*
|
|
2770
|
-
*
|
|
2771
|
-
*
|
|
2780
|
+
* topics hold a draft in __system__ that is not saved (C_TREEDB's
|
|
2781
|
+
* saved-schema, `draft_changed`). The mark of a write lived in this
|
|
2782
|
+
* session's memory only, so a reload of the page, a reconnect or a
|
|
2783
|
+
* refresh showed no draft while __system__ still differed (N13 of
|
|
2784
|
+
* the 2026-09-22 review).
|
|
2785
|
+
*
|
|
2786
|
+
* REPLACES what the host said before -- the host is the truth after
|
|
2787
|
+
* a reload, and a Save is exactly the host saying "none any more".
|
|
2788
|
+
* It only ever added, and a topic stayed a draft after the Save that
|
|
2789
|
+
* published it (M1 of the 2026-09-23 review). What this session wrote
|
|
2790
|
+
* stays marked: it is kept apart (see topic_is_draft()).
|
|
2772
2791
|
***************************************************************/
|
|
2773
2792
|
function ac_drafts(gobj, event, kw, src)
|
|
2774
2793
|
{
|
|
2775
2794
|
let priv = gobj.priv;
|
|
2776
2795
|
|
|
2777
2796
|
priv.host_drafts = (kw && kw.drafts && typeof kw.drafts === "object")? kw.drafts: {};
|
|
2778
|
-
|
|
2797
|
+
priv.host_ids = host_draft_ids(priv.model, priv.host_drafts);
|
|
2779
2798
|
if(priv.model) {
|
|
2780
2799
|
render(gobj);
|
|
2781
2800
|
}
|
package/src/c_yui_shell.js
CHANGED
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
createElement2, empty_string, is_object, is_array, is_string,
|
|
44
44
|
refresh_language,
|
|
45
45
|
is_gobj,
|
|
46
|
+
gobj_has_event,
|
|
47
|
+
gobj_is_destroying,
|
|
46
48
|
} from "@yuneta/gobj-js";
|
|
47
49
|
|
|
48
50
|
import {
|
|
@@ -174,6 +176,10 @@ let PRIVATE_DATA = {
|
|
|
174
176
|
* backend-connected state. State is host/event-driven (unlike
|
|
175
177
|
* the avatar provider it is a setter, not a pull callback). */
|
|
176
178
|
conn_nodes: [],
|
|
179
|
+
/* The last state yui_shell_set_connection_state() was told, null
|
|
180
|
+
* while nobody told it anything: EV_CONNECTION_STATE goes out on an
|
|
181
|
+
* EDGE, not on every call. */
|
|
182
|
+
connected: null,
|
|
177
183
|
/* Currently open toolbar dropdown panel, if any. Tracked here
|
|
178
184
|
* so a second click on any trigger (or programmatic close) can
|
|
179
185
|
* tear down the previous one through the same code path. */
|
|
@@ -3033,11 +3039,34 @@ function yui_shell_set_submenu(shell_gobj, parent_item_id, items)
|
|
|
3033
3039
|
/*---------------------------------------------*
|
|
3034
3040
|
* Global methods table
|
|
3035
3041
|
*---------------------------------------------*/
|
|
3042
|
+
/***************************************************************
|
|
3043
|
+
* EV_CONNECTION_STATE is an OPTIONAL fact: a view that waits for a
|
|
3044
|
+
* backend answer subscribes to it by name. An app that subscribed
|
|
3045
|
+
* to EVERY event of its shell with one attr (`subscriber`) did not
|
|
3046
|
+
* ask for it and does not declare it -- published to it anyway, the
|
|
3047
|
+
* event would be an FSM error in the app on every connection edge,
|
|
3048
|
+
* and adding it to the shell a breaking change for every such app.
|
|
3049
|
+
* So it reaches only the subscribers that declare it. Every other
|
|
3050
|
+
* event of the shell goes out as before.
|
|
3051
|
+
***************************************************************/
|
|
3052
|
+
function mt_publication_pre_filter(gobj, subs, event, kw)
|
|
3053
|
+
{
|
|
3054
|
+
if(event !== "EV_CONNECTION_STATE") {
|
|
3055
|
+
return 1;
|
|
3056
|
+
}
|
|
3057
|
+
let subscriber = subs && subs.subscriber;
|
|
3058
|
+
if(!subscriber || !gobj_has_event(subscriber, event, 0)) {
|
|
3059
|
+
return 0;
|
|
3060
|
+
}
|
|
3061
|
+
return 1;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3036
3064
|
const gmt = {
|
|
3037
|
-
mt_create:
|
|
3038
|
-
mt_start:
|
|
3039
|
-
mt_stop:
|
|
3040
|
-
mt_destroy:
|
|
3065
|
+
mt_create: mt_create,
|
|
3066
|
+
mt_start: mt_start,
|
|
3067
|
+
mt_stop: mt_stop,
|
|
3068
|
+
mt_destroy: mt_destroy,
|
|
3069
|
+
mt_publication_pre_filter: mt_publication_pre_filter
|
|
3041
3070
|
};
|
|
3042
3071
|
|
|
3043
3072
|
function create_gclass(gclass_name)
|
|
@@ -3088,6 +3117,13 @@ function create_gclass(gclass_name)
|
|
|
3088
3117
|
* re-renders its own translated parts. */
|
|
3089
3118
|
["EV_LANGUAGE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT
|
|
3090
3119
|
|event_flag_t.EVF_PUBLIC_EVENT
|
|
3120
|
+
|event_flag_t.EVF_NO_WARN_SUBS],
|
|
3121
|
+
|
|
3122
|
+
/* The app's backend connection went up or down
|
|
3123
|
+
* (yui_shell_set_connection_state), {connected}. Delivered only
|
|
3124
|
+
* to the subscribers that declare it: see
|
|
3125
|
+
* mt_publication_pre_filter(). */
|
|
3126
|
+
["EV_CONNECTION_STATE", event_flag_t.EVF_OUTPUT_EVENT
|
|
3091
3127
|
|event_flag_t.EVF_NO_WARN_SUBS]
|
|
3092
3128
|
];
|
|
3093
3129
|
|
|
@@ -3552,14 +3588,31 @@ function yui_shell_language_changed(shell_gobj)
|
|
|
3552
3588
|
************************************************************/
|
|
3553
3589
|
function yui_shell_set_connection_state(shell_gobj, connected)
|
|
3554
3590
|
{
|
|
3591
|
+
if(!shell_gobj || !is_gobj(shell_gobj) || gobj_is_destroying(shell_gobj)) {
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3555
3594
|
let priv = shell_gobj.priv;
|
|
3556
|
-
if(!priv
|
|
3595
|
+
if(!priv) {
|
|
3557
3596
|
return;
|
|
3558
3597
|
}
|
|
3559
3598
|
let on = !!connected;
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3599
|
+
let changed = (priv.connected !== on);
|
|
3600
|
+
priv.connected = on;
|
|
3601
|
+
if(is_array(priv.conn_nodes)) {
|
|
3602
|
+
for(let $n of priv.conn_nodes) {
|
|
3603
|
+
$n.classList.toggle("is-connected", on);
|
|
3604
|
+
$n.classList.toggle("is-disconnected", !on);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
/* The fact goes out as well, for the views: a write a view sent
|
|
3608
|
+
* before the backend dropped is never answered, and the view that
|
|
3609
|
+
* waits for it (a topic form's Save) learns the drop from HERE --
|
|
3610
|
+
* it holds no subscription of its own to the transport, because
|
|
3611
|
+
* subscribing to a C_IEVENT_CLI forwards the subscription to the
|
|
3612
|
+
* backend. Only an EDGE is published: the app calls this on every
|
|
3613
|
+
* reconnect attempt too. */
|
|
3614
|
+
if(changed) {
|
|
3615
|
+
gobj_publish_event(shell_gobj, "EV_CONNECTION_STATE", {connected: on});
|
|
3563
3616
|
}
|
|
3564
3617
|
}
|
|
3565
3618
|
|
|
@@ -99,7 +99,8 @@ import {t} from "i18next";
|
|
|
99
99
|
import {yui_toolbar_icon} from "./yui_toolbar.js";
|
|
100
100
|
|
|
101
101
|
import {
|
|
102
|
-
plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb
|
|
102
|
+
plan_treedb_writes, READONLY_FORM_TOOLBAR, col_goes_back_to_treedb,
|
|
103
|
+
instance_keys_of
|
|
103
104
|
} from "./treedb_write_plan.js";
|
|
104
105
|
import {
|
|
105
106
|
yui_file_read, yui_files_manifest, yui_file_id_label, yui_file_size_label
|
|
@@ -110,6 +111,7 @@ import {
|
|
|
110
111
|
import {delete_impact} from "./delete_impact.js";
|
|
111
112
|
|
|
112
113
|
import "./c_yui_treedb_topic_with_form.css";
|
|
114
|
+
import {yui_shell_show_error} from "./shell_modals.js";
|
|
113
115
|
import {set_toolbar_busy} from "./form_busy.js";
|
|
114
116
|
import "./tabulator.css";
|
|
115
117
|
import {cell_text, hook_cell_spec} from "./table_cell_text.js";
|
|
@@ -2394,6 +2396,10 @@ function open_form_dialog(gobj, mode, record)
|
|
|
2394
2396
|
style: 'display:flex; flex-direction:column; height:100%;'}, []]
|
|
2395
2397
|
);
|
|
2396
2398
|
|
|
2399
|
+
/* The instance this form writes to, kept as the record HAD it: the
|
|
2400
|
+
* widget is not trusted with an address (instance_keys_of()). */
|
|
2401
|
+
priv.form_instance = (form_mode === "update")? instance_keys_of(record, desc) : null;
|
|
2402
|
+
|
|
2397
2403
|
let form_plan = write_plan(gobj);
|
|
2398
2404
|
let form = gobj_create_pure_child(
|
|
2399
2405
|
"form_" + clean_name(gobj_name(gobj)),
|
|
@@ -2507,6 +2513,7 @@ function teardown_form_child(gobj)
|
|
|
2507
2513
|
priv.reading_files = 0; // a read in flight lands on nobody
|
|
2508
2514
|
priv.awaiting_write = 0; // and so does the answer of a write
|
|
2509
2515
|
priv.form_busy = {busy: false}; // its toolbar goes with it
|
|
2516
|
+
priv.form_instance = null;
|
|
2510
2517
|
if(priv.form) {
|
|
2511
2518
|
if(gobj_is_running(priv.form)) {
|
|
2512
2519
|
gobj_stop(priv.form);
|
|
@@ -3506,6 +3513,10 @@ function transform__form_value_2_treedb_value(gobj, col, value, operation)
|
|
|
3506
3513
|
case "string":
|
|
3507
3514
|
if(value && is_date(value)) {
|
|
3508
3515
|
value = value.toISOString();
|
|
3516
|
+
} else if(typeof value === "number" && value > 0) {
|
|
3517
|
+
/* The form hands an epoch in seconds (form_time_value);
|
|
3518
|
+
* a string column stores the instant as ISO text. */
|
|
3519
|
+
value = new Date(value * 1000).toISOString();
|
|
3509
3520
|
}
|
|
3510
3521
|
break;
|
|
3511
3522
|
case "integer":
|
|
@@ -4092,6 +4103,9 @@ function ac_confirmed(gobj, event, kw, src)
|
|
|
4092
4103
|
* them): never replaced by the row that took their place. */
|
|
4093
4104
|
log_error(`${gobj_short_name(gobj)}: ${event}: rows gone while ` +
|
|
4094
4105
|
`confirming the delete, not deleted: ${found.missing.join(", ")}`);
|
|
4106
|
+
/* The person who answered "yes" is told, not only the log
|
|
4107
|
+
* (a low of the 2026-09-22 review). */
|
|
4108
|
+
yui_shell_show_error(yui_shell_of(gobj), "some records were gone before the delete", {t: t});
|
|
4095
4109
|
}
|
|
4096
4110
|
if(!found.rows.length) {
|
|
4097
4111
|
return -1; /* Error already logged, or nothing was named */
|
|
@@ -4305,6 +4319,11 @@ function publish_treedb_write(gobj, mode, record, jn_files, form_write)
|
|
|
4305
4319
|
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
4306
4320
|
record: strip_read_only_cols(gobj, record)
|
|
4307
4321
|
};
|
|
4322
|
+
if(mode !== "create" && gobj.priv.form_instance) {
|
|
4323
|
+
/* The pkey2 names the instance: the value the record had, not
|
|
4324
|
+
* the one that came back through a datetime-local. */
|
|
4325
|
+
Object.assign(kw_write.record, gobj.priv.form_instance);
|
|
4326
|
+
}
|
|
4308
4327
|
if(form_write) {
|
|
4309
4328
|
/* The serial the host echoes back with the answer */
|
|
4310
4329
|
kw_write.form_write = form_write;
|
|
@@ -156,10 +156,11 @@ let __gclass__ = null;
|
|
|
156
156
|
***************************************************************/
|
|
157
157
|
function mt_create(gobj)
|
|
158
158
|
{
|
|
159
|
-
/* The writes of topic forms sent and not yet answered, by
|
|
160
|
-
* (form_writes_in_flight): answered refused when the
|
|
161
|
-
* closes under them. */
|
|
159
|
+
/* The writes of topic forms sent and not yet answered, by topic
|
|
160
|
+
* and serial (form_writes_in_flight): answered refused when the
|
|
161
|
+
* transport closes under them. */
|
|
162
162
|
gobj.priv.form_writes = {};
|
|
163
|
+
gobj.priv.conn_shell = null; /* the shell whose EV_CONNECTION_STATE we hear */
|
|
163
164
|
|
|
164
165
|
build_ui(gobj);
|
|
165
166
|
|
|
@@ -178,6 +179,17 @@ function mt_create(gobj)
|
|
|
178
179
|
***************************************************************/
|
|
179
180
|
function mt_start(gobj)
|
|
180
181
|
{
|
|
182
|
+
/* The app's backend connection, as the app tells its shell
|
|
183
|
+
* (yui_shell_set_connection_state). A write in flight when it drops
|
|
184
|
+
* is never answered, and its form waited for ever in every host
|
|
185
|
+
* that did not forward EV_TRANSPORT_STATE -- wattyzer and yunovatios
|
|
186
|
+
* among them (M8 of the 2026-09-23 review). The transport itself is
|
|
187
|
+
* not subscribed to: see ac_transport_state(). */
|
|
188
|
+
let shell = yui_shell_of(gobj);
|
|
189
|
+
if(shell) {
|
|
190
|
+
gobj_subscribe_event(shell, "EV_CONNECTION_STATE", {}, gobj);
|
|
191
|
+
gobj.priv.conn_shell = shell;
|
|
192
|
+
}
|
|
181
193
|
request_treedb_descs(gobj);
|
|
182
194
|
}
|
|
183
195
|
|
|
@@ -186,6 +198,14 @@ function mt_start(gobj)
|
|
|
186
198
|
***************************************************************/
|
|
187
199
|
function mt_stop(gobj)
|
|
188
200
|
{
|
|
201
|
+
let priv = gobj.priv;
|
|
202
|
+
if(priv.conn_shell) {
|
|
203
|
+
if(!gobj_is_destroying(priv.conn_shell)) {
|
|
204
|
+
gobj_unsubscribe_event(priv.conn_shell, "EV_CONNECTION_STATE", {}, gobj);
|
|
205
|
+
}
|
|
206
|
+
priv.conn_shell = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
189
209
|
/* Retire our site-map sub-routes so a torn-down view leaves no
|
|
190
210
|
* stale children in the map (ROUTING.md contributor). */
|
|
191
211
|
let shell = yui_shell_of(gobj);
|
|
@@ -2077,7 +2097,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2077
2097
|
{topic_name: failed_topic}, gobj);
|
|
2078
2098
|
}
|
|
2079
2099
|
settle_form_write(gobj.priv.form_writes,
|
|
2080
|
-
kw_get_int(gobj, kw_command, "form_write", 0, 0));
|
|
2100
|
+
kw_get_int(gobj, kw_command, "form_write", 0, 0), failed_topic);
|
|
2081
2101
|
answer_form_write(
|
|
2082
2102
|
gobj,
|
|
2083
2103
|
get_gobj_formtable(gobj, failed_topic),
|
|
@@ -2177,7 +2197,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2177
2197
|
command: command
|
|
2178
2198
|
});
|
|
2179
2199
|
settle_form_write(gobj.priv.form_writes,
|
|
2180
|
-
kw_get_int(gobj, kw_command, "form_write", 0, 0)
|
|
2200
|
+
kw_get_int(gobj, kw_command, "form_write", 0, 0),
|
|
2201
|
+
kw_get_str(gobj, kw_command, "topic_name", "", 0));
|
|
2181
2202
|
answer_form_write(
|
|
2182
2203
|
gobj,
|
|
2183
2204
|
get_gobj_formtable(gobj, kw_get_str(gobj, kw_command, "topic_name", "", 0)),
|
|
@@ -2413,11 +2434,19 @@ function ac_back_to_topics(gobj, event, kw, src)
|
|
|
2413
2434
|
}
|
|
2414
2435
|
|
|
2415
2436
|
/************************************************************
|
|
2416
|
-
*
|
|
2417
|
-
*
|
|
2418
|
-
*
|
|
2419
|
-
*
|
|
2420
|
-
*
|
|
2437
|
+
* A backend transport edge, from either of two places:
|
|
2438
|
+
* - EV_TRANSPORT_STATE, forwarded by a host that keeps one
|
|
2439
|
+
* transport per view (gui_treedb's C_TREEDB_VIEW, gui_agent's
|
|
2440
|
+
* C_AGENT_TREEDB_VIEW);
|
|
2441
|
+
* - EV_CONNECTION_STATE, published by the SHELL when the app tells
|
|
2442
|
+
* it its backend went up or down (yui_shell_set_connection_state),
|
|
2443
|
+
* which is what every single-backend app already does for the
|
|
2444
|
+
* toolbar dot. No host has to remember anything for this one.
|
|
2445
|
+
* Both can arrive for one edge; what is done here is idempotent.
|
|
2446
|
+
* The toolbar disables the JSON viewers the moment the session drops
|
|
2447
|
+
* (and re-enables them on reconnect). The library view must not
|
|
2448
|
+
* subscribe to the C_IEVENT_CLI itself: that forwards the subscription
|
|
2449
|
+
* upstream and breaks the session.
|
|
2421
2450
|
************************************************************/
|
|
2422
2451
|
function ac_transport_state(gobj, event, kw, src)
|
|
2423
2452
|
{
|
|
@@ -3042,6 +3071,7 @@ function create_gclass(gclass_name)
|
|
|
3042
3071
|
["EV_HIDE", ac_hide, null],
|
|
3043
3072
|
["EV_BACK_TO_TOPICS", ac_back_to_topics, null],
|
|
3044
3073
|
["EV_TRANSPORT_STATE", ac_transport_state, null],
|
|
3074
|
+
["EV_CONNECTION_STATE", ac_transport_state, null],
|
|
3045
3075
|
]]
|
|
3046
3076
|
];
|
|
3047
3077
|
|
|
@@ -3077,6 +3107,7 @@ function create_gclass(gclass_name)
|
|
|
3077
3107
|
["EV_HIDE", 0],
|
|
3078
3108
|
["EV_BACK_TO_TOPICS", 0],
|
|
3079
3109
|
["EV_TRANSPORT_STATE", 0],
|
|
3110
|
+
["EV_CONNECTION_STATE", 0],
|
|
3080
3111
|
["EV_TOPIC_SELECTED",
|
|
3081
3112
|
event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
|
|
3082
3113
|
];
|
package/src/form_time_value.js
CHANGED
|
@@ -6,6 +6,29 @@
|
|
|
6
6
|
* them, and they must be each other's inverse: a writable time
|
|
7
7
|
* column travels back on EVERY save of the record.
|
|
8
8
|
*
|
|
9
|
+
* KNOWN LIMIT -- the change of hour. A `datetime-local` holds a
|
|
10
|
+
* LOCAL WALL time and no offset, and a wall time is not always
|
|
11
|
+
* one instant:
|
|
12
|
+
*
|
|
13
|
+
* - the night the clocks go back (autumn), one hour happens
|
|
14
|
+
* twice. "02:30" names two instants an hour apart, and
|
|
15
|
+
* `new Date("YYYY-MM-DDT02:30:00")` answers the EARLIER one
|
|
16
|
+
* (ECMAScript's "compatible" disambiguation). An epoch in the
|
|
17
|
+
* SECOND pass of that hour is shown as 02:30 and comes back
|
|
18
|
+
* one hour earlier: saving the record -- any field of it --
|
|
19
|
+
* moves a writable time column that holds it by -3600 s.
|
|
20
|
+
* - the night the clocks go forward (spring), one hour does not
|
|
21
|
+
* exist. No epoch is shown in it, so nothing read comes back
|
|
22
|
+
* wrong; a wall time TYPED in it is moved forward by the
|
|
23
|
+
* browser.
|
|
24
|
+
*
|
|
25
|
+
* Not fixable here without the offset, which the input cannot
|
|
26
|
+
* carry. What is safe: a read-only time column never goes back
|
|
27
|
+
* (treedb_write_plan.js, col_goes_back_to_treedb), and a time
|
|
28
|
+
* pkey2 goes back as the record had it, never through this
|
|
29
|
+
* widget (instance_keys_of). A writable time column edited or
|
|
30
|
+
* merely saved inside the repeated hour is the exposed case.
|
|
31
|
+
*
|
|
9
32
|
* Copyright (c) 2026, ArtGins.
|
|
10
33
|
* All Rights Reserved.
|
|
11
34
|
****************************************************************************/
|
|
@@ -2,18 +2,33 @@
|
|
|
2
2
|
* form_writes_in_flight.js
|
|
3
3
|
*
|
|
4
4
|
* The writes of topic forms a C_YUI_TREEDB_TOPICS view has sent
|
|
5
|
-
* and not yet answered
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* and not yet answered. A form that waits for its answer
|
|
6
|
+
* (form_waits_for_answer) stays open and busy until the host
|
|
7
|
+
* answers, so the host has to answer EVERY way a write ends --
|
|
8
|
+
* and the transport closing under a write in flight, or a
|
|
9
|
+
* command that never left because there was no session, are two
|
|
10
|
+
* of them that nobody answered: the form stayed busy for ever
|
|
11
|
+
* (N8 of the 2026-09-22 review).
|
|
12
|
+
*
|
|
13
|
+
* A write is known by its TOPIC and its `form_write` serial,
|
|
14
|
+
* never by the serial alone: each topic has its own form, and
|
|
15
|
+
* each form counts its serials from 1. Keyed by the serial, two
|
|
16
|
+
* forms with a write in flight at once shared the key "1", the
|
|
17
|
+
* first answer settled both, and the transport closing then
|
|
18
|
+
* answered only one of them.
|
|
12
19
|
*
|
|
13
20
|
* Copyright (c) 2026, ArtGins.
|
|
14
21
|
* All Rights Reserved.
|
|
15
22
|
***********************************************************************/
|
|
16
23
|
|
|
24
|
+
/************************************************************
|
|
25
|
+
* The key of one write: `<topic>^<serial>`.
|
|
26
|
+
************************************************************/
|
|
27
|
+
function form_write_key(topic_name, form_write)
|
|
28
|
+
{
|
|
29
|
+
return `${topic_name || ""}^${form_write}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
/************************************************************
|
|
18
33
|
* A write left: remember whose it is
|
|
19
34
|
************************************************************/
|
|
@@ -22,18 +37,21 @@ function track_form_write(in_flight, form_write, topic_name)
|
|
|
22
37
|
if(!in_flight || !form_write) {
|
|
23
38
|
return;
|
|
24
39
|
}
|
|
25
|
-
in_flight[form_write] =
|
|
40
|
+
in_flight[form_write_key(topic_name, form_write)] = {
|
|
41
|
+
form_write: form_write,
|
|
42
|
+
topic_name: topic_name || ""
|
|
43
|
+
};
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
/************************************************************
|
|
29
47
|
* A write was answered, one way or the other
|
|
30
48
|
************************************************************/
|
|
31
|
-
function settle_form_write(in_flight, form_write)
|
|
49
|
+
function settle_form_write(in_flight, form_write, topic_name)
|
|
32
50
|
{
|
|
33
51
|
if(!in_flight || !form_write) {
|
|
34
52
|
return;
|
|
35
53
|
}
|
|
36
|
-
delete in_flight[form_write];
|
|
54
|
+
delete in_flight[form_write_key(topic_name, form_write)];
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
/************************************************************
|
|
@@ -46,9 +64,10 @@ function abandon_form_writes(in_flight)
|
|
|
46
64
|
if(!in_flight) {
|
|
47
65
|
return abandoned;
|
|
48
66
|
}
|
|
49
|
-
Object.keys(in_flight).forEach((
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
Object.keys(in_flight).forEach((key) => {
|
|
68
|
+
const w = in_flight[key];
|
|
69
|
+
abandoned.push({form_write: Number(w.form_write), topic_name: w.topic_name});
|
|
70
|
+
delete in_flight[key];
|
|
52
71
|
});
|
|
53
72
|
return abandoned;
|
|
54
73
|
}
|
|
@@ -11,8 +11,23 @@ describe("form writes in flight", () => {
|
|
|
11
11
|
const in_flight = {};
|
|
12
12
|
track_form_write(in_flight, 1, "users");
|
|
13
13
|
track_form_write(in_flight, 2, "roles");
|
|
14
|
-
settle_form_write(in_flight, 1);
|
|
15
|
-
expect(
|
|
14
|
+
settle_form_write(in_flight, 1, "users");
|
|
15
|
+
expect(abandon_form_writes(in_flight)).toEqual([
|
|
16
|
+
{form_write: 2, topic_name: "roles"},
|
|
17
|
+
]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("two forms, each at its serial 1: two writes, not one", () => {
|
|
21
|
+
/* Each topic's form counts its serials from 1. Keyed by the serial
|
|
22
|
+
* alone, the answer of one form settled the other's write, and a
|
|
23
|
+
* transport closing then left that one busy for ever. */
|
|
24
|
+
const in_flight = {};
|
|
25
|
+
track_form_write(in_flight, 1, "users");
|
|
26
|
+
track_form_write(in_flight, 1, "roles");
|
|
27
|
+
settle_form_write(in_flight, 1, "users");
|
|
28
|
+
expect(abandon_form_writes(in_flight)).toEqual([
|
|
29
|
+
{form_write: 1, topic_name: "roles"},
|
|
30
|
+
]);
|
|
16
31
|
});
|
|
17
32
|
|
|
18
33
|
it("the transport closes: every write in flight is abandoned, once (N8)", () => {
|
|
@@ -39,7 +54,7 @@ describe("form writes in flight", () => {
|
|
|
39
54
|
|
|
40
55
|
it("no map: nothing to do", () => {
|
|
41
56
|
expect(() => track_form_write(null, 1, "x")).not.toThrow();
|
|
42
|
-
expect(() => settle_form_write(null, 1)).not.toThrow();
|
|
57
|
+
expect(() => settle_form_write(null, 1, "x")).not.toThrow();
|
|
43
58
|
expect(abandon_form_writes(null)).toEqual([]);
|
|
44
59
|
});
|
|
45
60
|
});
|
package/src/host_drafts.js
CHANGED
|
@@ -10,17 +10,42 @@
|
|
|
10
10
|
* __system__ still differed from the file in use (N13 of the
|
|
11
11
|
* 2026-09-22 review). The host reads that difference from
|
|
12
12
|
* C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
|
|
13
|
-
* hands it here; the editor applies it to its model
|
|
13
|
+
* hands it here; the editor applies it to its model, in place
|
|
14
|
+
* of what the host said before.
|
|
15
|
+
*
|
|
16
|
+
* What `draft_changed` measures depends on the node: the draft
|
|
17
|
+
* against the SAVED schema on a node newer than yunetas 7.25.3,
|
|
18
|
+
* and against the file IN USE on a 7.25.3 node -- where a topic saved and
|
|
19
|
+
* not yet applied is still named, and the editor, which cannot
|
|
20
|
+
* tell the two apart, shows it as a draft until the Apply.
|
|
14
21
|
*
|
|
15
22
|
* Copyright (c) 2026, ArtGins.
|
|
16
23
|
* All Rights Reserved.
|
|
17
24
|
***********************************************************************/
|
|
18
25
|
|
|
26
|
+
/************************************************************
|
|
27
|
+
* The topics of `model` the host names as drafts, as
|
|
28
|
+
* {topic id: true}: `drafts` is {treedb_name: [topic names]}. A
|
|
29
|
+
* treedb or a topic the model does not hold is ignored.
|
|
30
|
+
*
|
|
31
|
+
* What the editor keeps is THIS, apart from what its own session
|
|
32
|
+
* wrote, and replaced whole on every EV_DRAFTS: the host is the
|
|
33
|
+
* truth after a reload. Folded into the session's marks, a topic the
|
|
34
|
+
* host once named stayed a draft after the save that published it.
|
|
35
|
+
************************************************************/
|
|
36
|
+
function host_draft_ids(model, drafts)
|
|
37
|
+
{
|
|
38
|
+
const ids = {};
|
|
39
|
+
mark_host_drafts(ids, model, drafts);
|
|
40
|
+
return ids;
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
/************************************************************
|
|
20
44
|
* Mark in `written` the topics of `model` the host names as
|
|
21
45
|
* drafts: `drafts` is {treedb_name: [topic names]}. A treedb or a
|
|
22
46
|
* topic the model does not hold is ignored. Returns how many were
|
|
23
|
-
* marked.
|
|
47
|
+
* marked. It only ADDS: a caller that must forget what the host
|
|
48
|
+
* no longer names starts from an empty map (host_draft_ids()).
|
|
24
49
|
************************************************************/
|
|
25
50
|
function mark_host_drafts(written, model, drafts)
|
|
26
51
|
{
|
|
@@ -63,6 +88,7 @@ function drafts_of_saved_answer(rows, into)
|
|
|
63
88
|
}
|
|
64
89
|
|
|
65
90
|
export {
|
|
91
|
+
host_draft_ids,
|
|
66
92
|
mark_host_drafts,
|
|
67
93
|
drafts_of_saved_answer,
|
|
68
94
|
};
|