@yuneta/gobj-ui 7.25.14 → 7.25.15
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 +41 -0
- package/dist/gobj-ui.cjs.js +184 -53
- package/dist/gobj-ui.es.js +184 -53
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +42 -11
- package/src/c_yui_schema_editor.js +104 -37
- package/src/c_yui_treedb_graph.js +53 -6
- package/src/c_yui_treedb_topic_with_form.js +10 -10
- package/src/c_yui_treedb_topics.js +7 -7
- package/src/form_busy.js +2 -3
- package/src/form_time_value.js +1 -1
- package/src/form_time_value.test.js +1 -1
- package/src/form_writes_in_flight.js +1 -2
- package/src/g6_nodes_tree_fsm.test.js +33 -0
- package/src/graph_save_plan.js +28 -0
- package/src/graph_save_plan.test.js +36 -2
- package/src/host_drafts.js +2 -3
- package/src/schema_editor_drafts.wiring.test.js +1 -1
- package/src/schema_editor_reload.wiring.test.js +178 -46
- package/src/schema_editor_transport.wiring.test.js +2 -2
- package/src/schema_version_bump.test.js +1 -1
- package/src/shell_modals.js +2 -3
- package/src/shell_toast_once.test.js +10 -10
- package/src/table_cell_text.js +1 -1
- package/src/treedb_graph_writes.wiring.test.js +208 -0
- package/src/treedb_topics_transport.wiring.test.js +1 -1
- package/src/yui_row_search.test.js +1 -1
- package/src/yui_table_select.test.js +1 -1
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
* AN EDIT IS A DRAFT. A write here moves no version and reaches no
|
|
23
23
|
* treedb: the host SAVES it (C_TREEDB's save-schema raises the
|
|
24
24
|
* topic_version of what changed and the schema_version, once) and
|
|
25
|
-
* APPLIES it (apply-schema + a restart of the owning yuno).
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* wrote and has not saved (EV_REFRESH forgets it: the view's own
|
|
25
|
+
* APPLIES it (apply-schema + a restart of the owning yuno). If a
|
|
26
|
+
* write here raised both versions, an edit half made would already
|
|
27
|
+
* be the schema of the next start. The topic list marks what this
|
|
28
|
+
* session wrote and has not saved (EV_REFRESH forgets it: the view's own
|
|
30
29
|
* Refresh button sends it, and a host may after a save -- the
|
|
31
30
|
* agent console's does not) and what the host says is not saved
|
|
32
31
|
* (EV_DRAFTS, replaced whole each time it is sent).
|
|
@@ -188,6 +187,11 @@ const DIAGRAM_SEG = "diagram";
|
|
|
188
187
|
* them), and each filters on its own purpose. */
|
|
189
188
|
const PURPOSE = "schema_editor";
|
|
190
189
|
|
|
190
|
+
/* The dialogs that show what they were opened with and send nothing a
|
|
191
|
+
* screen declares: a move sent by the host leaves them up (see
|
|
192
|
+
* close_dialog_left_behind()). */
|
|
193
|
+
const DIALOGS_THAT_READ_NOTHING = ["SCHEMA_EXPORT_DIALOG", "SCHEMA_REPORT_DIALOG"];
|
|
194
|
+
|
|
191
195
|
|
|
192
196
|
/***************************************************************
|
|
193
197
|
* Attrs
|
|
@@ -230,6 +234,7 @@ let PRIVATE_DATA = {
|
|
|
230
234
|
diagram_gobj: null,
|
|
231
235
|
dialog: null,
|
|
232
236
|
dialog_gen: 0, /* the model_gen the open dialog was built on */
|
|
237
|
+
dialog_kind: "", /* the logical class of the open dialog */
|
|
233
238
|
|
|
234
239
|
/* A write, or a plan of them, in flight */
|
|
235
240
|
save_queue: null,
|
|
@@ -460,9 +465,9 @@ function refuse_if_readonly(gobj, event)
|
|
|
460
465
|
* dumped by the machine trace).
|
|
461
466
|
*
|
|
462
467
|
* Refused, and said. A column form sends every field it shows, so
|
|
463
|
-
* its Save after a reload wrote the OLD record over the newer one
|
|
464
|
-
*
|
|
465
|
-
*
|
|
468
|
+
* its Save after a reload wrote the OLD record over the newer one;
|
|
469
|
+
* a form whose column the reload removed answered "names no column"
|
|
470
|
+
* for ever. end_load()
|
|
466
471
|
* closes such a dialog when the load lands; this is the same rule
|
|
467
472
|
* for whatever arrives anyway -- a stale screen, a confirmation
|
|
468
473
|
* that was up, a keyboard.
|
|
@@ -489,8 +494,7 @@ function built_on_a_replaced_model(gobj, event, kw)
|
|
|
489
494
|
* the model shown is kept, and it may be older than the store.
|
|
490
495
|
* The session is up, so no reconnect will ask for it again -- it
|
|
491
496
|
* was owed for as long as the session lasted, and a form opened
|
|
492
|
-
* on that model wrote every field of it over the newer record
|
|
493
|
-
* (fifth independent review; A3 of the fourth, by another path).
|
|
497
|
+
* on that model wrote every field of it over the newer record.
|
|
494
498
|
*
|
|
495
499
|
* So the operator's NEXT action asks it. An edit -- a form, a
|
|
496
500
|
* write, a confirmation, what reads the model whole (check,
|
|
@@ -562,7 +566,7 @@ function reload_after_move(gobj)
|
|
|
562
566
|
* none of the actions of a screen, so the screen it replaces goes
|
|
563
567
|
* and the toolbar stops answering: left up, a click on a card, on
|
|
564
568
|
* Back or on the drawing answered "Event NOT DEFINED in state
|
|
565
|
-
* ST_LOADING" and was lost
|
|
569
|
+
* ST_LOADING" and was lost. A dialog
|
|
566
570
|
* left open is answered in ST_LOADING instead (ac_wait_for_the_load),
|
|
567
571
|
* and a load that LANDS closes it (end_load()): it was built on the
|
|
568
572
|
* model the load replaced, and its Save sends every field it shows.
|
|
@@ -969,8 +973,7 @@ function render_toolbar(gobj)
|
|
|
969
973
|
* reload that no longer has them keeps the position (it says so,
|
|
970
974
|
* and Back is the way out). Asked of the MODEL, not of the state:
|
|
971
975
|
* gated on the state, Diagram, Check, Export, Import and New topic
|
|
972
|
-
* stayed up there and each logged "with no treedb open"
|
|
973
|
-
* independent review, probe D2). */
|
|
976
|
+
* stayed up there and each logged "with no treedb open". */
|
|
974
977
|
let treedb_here = on_a_treedb && !!current_treedb(gobj);
|
|
975
978
|
let topic_here = treedb_here && !!current_topic(gobj);
|
|
976
979
|
/* Nothing is legal while the model is in the air: every action is
|
|
@@ -1745,6 +1748,7 @@ function close_dialog(gobj)
|
|
|
1745
1748
|
}
|
|
1746
1749
|
let dialog = priv.dialog;
|
|
1747
1750
|
priv.dialog = null;
|
|
1751
|
+
priv.dialog_kind = "";
|
|
1748
1752
|
priv.export_view = null;
|
|
1749
1753
|
try {
|
|
1750
1754
|
dialog.close();
|
|
@@ -1759,6 +1763,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
|
|
|
1759
1763
|
|
|
1760
1764
|
close_dialog(gobj);
|
|
1761
1765
|
priv.dialog_gen = priv.model_gen;
|
|
1766
|
+
priv.dialog_kind = logical;
|
|
1762
1767
|
priv.dialog = yui_shell_show_modal(yui_shell_of(gobj), $content, {
|
|
1763
1768
|
dialog: true,
|
|
1764
1769
|
title: title,
|
|
@@ -1767,6 +1772,7 @@ function open_dialog(gobj, $content, title, logical, title_prefix)
|
|
|
1767
1772
|
t: t,
|
|
1768
1773
|
on_close: () => {
|
|
1769
1774
|
priv.dialog = null;
|
|
1775
|
+
priv.dialog_kind = "";
|
|
1770
1776
|
priv.export_view = null;
|
|
1771
1777
|
}
|
|
1772
1778
|
});
|
|
@@ -2674,11 +2680,11 @@ function transport_in_session(gobj)
|
|
|
2674
2680
|
* The state is part of the question, and that is only true
|
|
2675
2681
|
* because NOTHING moves the editor out of ST_LOADING or ST_SAVING
|
|
2676
2682
|
* but the end of what is in flight: a position the host sends
|
|
2677
|
-
* meanwhile waits in `pending_seg` (ac_show()).
|
|
2678
|
-
*
|
|
2679
|
-
*
|
|
2680
|
-
*
|
|
2681
|
-
*
|
|
2683
|
+
* meanwhile waits in `pending_seg` (ac_show()). If EV_SHOW moved a
|
|
2684
|
+
* RELOAD out of ST_LOADING, every answer of it would be "for a load
|
|
2685
|
+
* that is over": the records it had emptied would stay empty, and
|
|
2686
|
+
* the next write would patch them into a model with no treedb at
|
|
2687
|
+
* all.
|
|
2682
2688
|
***************************************************************/
|
|
2683
2689
|
function is_current_load(gobj, md)
|
|
2684
2690
|
{
|
|
@@ -2995,12 +3001,74 @@ function position_seg(gobj)
|
|
|
2995
3001
|
* ...and the same in reverse. A tail naming a treedb or a topic
|
|
2996
3002
|
* the store does not have lands one level up rather than on an
|
|
2997
3003
|
* empty screen: a shared link outlives the schema it points at.
|
|
3004
|
+
*
|
|
3005
|
+
* Every position the HOST sends comes through here, at once
|
|
3006
|
+
* (ac_show()) or when what was in flight ends (end_load(),
|
|
3007
|
+
* end_writes()). A move to another position closes the dialog of
|
|
3008
|
+
* the screen the editor left (close_dialog_left_behind()).
|
|
2998
3009
|
***************************************************************/
|
|
2999
3010
|
function apply_seg(gobj, seg)
|
|
3000
3011
|
{
|
|
3012
|
+
let before = position_seg(gobj);
|
|
3001
3013
|
let pos = seg_position(seg);
|
|
3002
3014
|
|
|
3003
|
-
|
|
3015
|
+
let ret = go(gobj, pos.treedb_id, pos.topic_name, pos.diagram, false);
|
|
3016
|
+
if(position_seg(gobj) !== before) {
|
|
3017
|
+
close_dialog_left_behind(gobj);
|
|
3018
|
+
}
|
|
3019
|
+
return ret;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
/***************************************************************
|
|
3023
|
+
* The host moved the editor while a dialog was up. C_YUI_SHELL
|
|
3024
|
+
* keeps the overlays open on a move that changes only the subpath
|
|
3025
|
+
* (a url typed in, a link), so the dialog outlives its screen.
|
|
3026
|
+
*
|
|
3027
|
+
* A form, the import and the orphans send events that only the
|
|
3028
|
+
* screen they were opened from declares, and they name their
|
|
3029
|
+
* topic under the treedb open at the time. Left up on another
|
|
3030
|
+
* screen, their Save answered "Event NOT DEFINED" and the edit was
|
|
3031
|
+
* lost with no word. So the dialog is closed and the operator told.
|
|
3032
|
+
* The import plan goes with its dialog: it was computed for the
|
|
3033
|
+
* treedb the editor left.
|
|
3034
|
+
*
|
|
3035
|
+
* The export and the check stay: they show the texts they were
|
|
3036
|
+
* opened with, and what they send is legal in every state.
|
|
3037
|
+
***************************************************************/
|
|
3038
|
+
function close_dialog_left_behind(gobj)
|
|
3039
|
+
{
|
|
3040
|
+
let priv = gobj.priv;
|
|
3041
|
+
|
|
3042
|
+
if(!priv.dialog || DIALOGS_THAT_READ_NOTHING.includes(priv.dialog_kind)) {
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
log_warning(`${gobj_short_name(gobj)}: the host moved the view while the dialog ` +
|
|
3046
|
+
`${priv.dialog_kind} was open: closed`);
|
|
3047
|
+
close_dialog(gobj);
|
|
3048
|
+
forget_import_plan(gobj);
|
|
3049
|
+
yui_shell_show_error(yui_shell_of(gobj), "the view moved: open the dialog again", {t: t});
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
/***************************************************************
|
|
3053
|
+
* A confirmation answered on another screen than the one it was
|
|
3054
|
+
* asked from. A confirmation is a shell modal, so a move sent by
|
|
3055
|
+
* the host does not close it. It names its topic and column, and
|
|
3056
|
+
* the actions look them up under the treedb open NOW: a Yes after
|
|
3057
|
+
* a move to another treedb deleted the topic or column of the same
|
|
3058
|
+
* name THERE.
|
|
3059
|
+
*
|
|
3060
|
+
* Refused, and said. A kw with no `seg` stamp is not a
|
|
3061
|
+
* confirmation's and is not asked.
|
|
3062
|
+
***************************************************************/
|
|
3063
|
+
function asked_on_another_screen(gobj, event, kw)
|
|
3064
|
+
{
|
|
3065
|
+
if(!kw || kw.seg === undefined || kw.seg === position_seg(gobj)) {
|
|
3066
|
+
return false;
|
|
3067
|
+
}
|
|
3068
|
+
log_warning(`${gobj_short_name(gobj)}: ${event} refused, it was asked on ` +
|
|
3069
|
+
`'${kw.seg}' and the view is on '${position_seg(gobj)}'`);
|
|
3070
|
+
yui_shell_show_error(yui_shell_of(gobj), "the view moved: open the dialog again", {t: t});
|
|
3071
|
+
return true;
|
|
3004
3072
|
}
|
|
3005
3073
|
|
|
3006
3074
|
function seg_position(seg)
|
|
@@ -3111,10 +3179,10 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
3111
3179
|
* slow `nodes`): the model shown is what the store held a
|
|
3112
3180
|
* moment ago, and blanking it took the screen and every open
|
|
3113
3181
|
* form with it -- a form's Save then answered "Event NOT
|
|
3114
|
-
* DEFINED in state ST_IDLE"
|
|
3182
|
+
* DEFINED in state ST_IDLE". It
|
|
3115
3183
|
* stays, as it does when the load cannot leave, and the
|
|
3116
3184
|
* reload is owed: the operator's next action asks it
|
|
3117
|
-
* (reload_first()
|
|
3185
|
+
* (reload_first()). */
|
|
3118
3186
|
if(priv.model) {
|
|
3119
3187
|
log_warning(`${gobj_short_name(gobj)}: the schemas could not be read ` +
|
|
3120
3188
|
`again (${priv.load_error}): the previous ones stay`);
|
|
@@ -3139,13 +3207,13 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
3139
3207
|
/* The store was read whole: whatever reload was owed is paid,
|
|
3140
3208
|
* whoever asked for this one -- the operator's Refresh, a late
|
|
3141
3209
|
* write, the reconnect. Left owed, the next edit was refused
|
|
3142
|
-
* and read the store AGAIN
|
|
3210
|
+
* and read the store AGAIN. */
|
|
3143
3211
|
priv.reload_on_open = false;
|
|
3144
3212
|
/* The plan was a list of writes computed on the model this load
|
|
3145
3213
|
* replaces. Forgotten HERE, where the model goes, and not when
|
|
3146
3214
|
* the requests leave: a load that fails replaces nothing, and
|
|
3147
3215
|
* forgetting early left its dialog up with no plan and Import
|
|
3148
|
-
* disabled
|
|
3216
|
+
* disabled. */
|
|
3149
3217
|
forget_import_plan(gobj);
|
|
3150
3218
|
build_model(gobj);
|
|
3151
3219
|
/* Same reason as order_undo above: this is the LOAD, and the
|
|
@@ -3191,7 +3259,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
3191
3259
|
* silence, a drag of five columns stopped at the first.
|
|
3192
3260
|
* The reload is also the one a late write may have owed
|
|
3193
3261
|
* (reload_after_write): left set, the next write loaded
|
|
3194
|
-
* the whole model once more
|
|
3262
|
+
* the whole model once more. */
|
|
3195
3263
|
let not_sent = priv.save_queue.length - 1;
|
|
3196
3264
|
log_warning(`${gobj_short_name(gobj)}: '${command}' answered no record` +
|
|
3197
3265
|
(not_sent > 0 ? `: ${not_sent} write(s) after it were not sent` : ""));
|
|
@@ -3284,8 +3352,7 @@ function late_write_done(gobj, command, topic_name, data)
|
|
|
3284
3352
|
* The fkey is read with parse_fkey_ref(), because the store answers
|
|
3285
3353
|
* in list_dict -- `[{topic_name, id, hook_name}]` -- and not in the
|
|
3286
3354
|
* `topics^<topic id>^cols` strings this view writes. Split as a
|
|
3287
|
-
* string, every production answer marked nothing
|
|
3288
|
-
* review, probe C).
|
|
3355
|
+
* string, every production answer marked nothing.
|
|
3289
3356
|
***************************************************************/
|
|
3290
3357
|
function mark_written_record(gobj, topic_name, record)
|
|
3291
3358
|
{
|
|
@@ -3344,7 +3411,7 @@ function ac_show(gobj, event, kw, src)
|
|
|
3344
3411
|
/* A move like the editor's own (ac_back() and the rest): it goes,
|
|
3345
3412
|
* and pays the reload a refused load owes there. Without it a url
|
|
3346
3413
|
* or the browser's Back left it owed on a screen that asked for
|
|
3347
|
-
* nothing
|
|
3414
|
+
* nothing. */
|
|
3348
3415
|
apply_seg(gobj, seg);
|
|
3349
3416
|
return reload_after_move(gobj);
|
|
3350
3417
|
}
|
|
@@ -3368,7 +3435,7 @@ function ac_hide(gobj, event, kw, src)
|
|
|
3368
3435
|
*
|
|
3369
3436
|
* Nothing else ends a load or a write the drop cut: their answers
|
|
3370
3437
|
* will never come, and waiting in ST_LOADING skipped the reload
|
|
3371
|
-
* itself
|
|
3438
|
+
* itself.
|
|
3372
3439
|
***************************************************************/
|
|
3373
3440
|
function ac_transport_state(gobj, event, kw, src)
|
|
3374
3441
|
{
|
|
@@ -3417,12 +3484,11 @@ function ac_refresh(gobj, event, kw, src)
|
|
|
3417
3484
|
* a model the answers in the air are about to replace.
|
|
3418
3485
|
*
|
|
3419
3486
|
* Refused, and SAID. Undeclared, it answered "Event NOT DEFINED
|
|
3420
|
-
* in state ST_LOADING" and the edit was gone with no word
|
|
3421
|
-
* independent review).
|
|
3487
|
+
* in state ST_LOADING" and the edit was gone with no word.
|
|
3422
3488
|
*
|
|
3423
3489
|
* And NOT "try again": the same Save, once the load was in, sent
|
|
3424
3490
|
* the form built on the old model -- every field of the old record
|
|
3425
|
-
* over the newer one
|
|
3491
|
+
* over the newer one. The load that
|
|
3426
3492
|
* lands closes such a dialog (end_load()); one that fails leaves it
|
|
3427
3493
|
* up, on the model it was built on.
|
|
3428
3494
|
***************************************************************/
|
|
@@ -3439,8 +3505,7 @@ function ac_wait_for_the_load(gobj, event, kw, src)
|
|
|
3439
3505
|
* reload landed on zero treedbs while it was up) or ST_IDLE (no
|
|
3440
3506
|
* model). A confirmation is a shell modal, not this view's dialog,
|
|
3441
3507
|
* so the load that lands does not close it, and its Yes arrives
|
|
3442
|
-
* anyway. Undeclared, it answered "Event NOT DEFINED"
|
|
3443
|
-
* independent review).
|
|
3508
|
+
* anyway. Undeclared, it answered "Event NOT DEFINED".
|
|
3444
3509
|
*
|
|
3445
3510
|
* Refused with the stamp's own words: whatever it was decided on
|
|
3446
3511
|
* is not on screen any more.
|
|
@@ -3461,13 +3526,12 @@ function ac_confirmed_on_nothing(gobj, event, kw, src)
|
|
|
3461
3526
|
* topics hold a draft in __system__ that is not saved (C_TREEDB's
|
|
3462
3527
|
* saved-schema, `draft_changed`). The mark of a write lived in this
|
|
3463
3528
|
* session's memory only, so a reload of the page, a reconnect or a
|
|
3464
|
-
* refresh showed no draft while __system__ still differed
|
|
3465
|
-
* the 2026-09-22 review).
|
|
3529
|
+
* refresh showed no draft while __system__ still differed.
|
|
3466
3530
|
*
|
|
3467
3531
|
* REPLACES what the host said before -- the host is the truth after
|
|
3468
3532
|
* a reload, and a Save is exactly the host saying "none any more".
|
|
3469
3533
|
* It only ever added, and a topic stayed a draft after the Save that
|
|
3470
|
-
* published it
|
|
3534
|
+
* published it. What this session wrote
|
|
3471
3535
|
* stays marked: it is kept apart (see topic_is_draft()).
|
|
3472
3536
|
***************************************************************/
|
|
3473
3537
|
function ac_drafts(gobj, event, kw, src)
|
|
@@ -3552,7 +3616,7 @@ function ac_show_orphans(gobj, event, kw, src)
|
|
|
3552
3616
|
***************************************************************/
|
|
3553
3617
|
function confirm_then(gobj, message, detail, kw)
|
|
3554
3618
|
{
|
|
3555
|
-
kw = Object.assign({model_gen: gobj.priv.model_gen}, kw);
|
|
3619
|
+
kw = Object.assign({model_gen: gobj.priv.model_gen, seg: position_seg(gobj)}, kw);
|
|
3556
3620
|
/* The answers are i18n KEYS: shell_modals names each button by its
|
|
3557
3621
|
* label, and its defaults ("Delete", "Cancel") are not lower-case, so
|
|
3558
3622
|
* no validated locale holds them -- they read English in every
|
|
@@ -4092,6 +4156,9 @@ function ac_confirmed(gobj, event, kw, src)
|
|
|
4092
4156
|
if(built_on_a_replaced_model(gobj, event, kw)) {
|
|
4093
4157
|
return -1;
|
|
4094
4158
|
}
|
|
4159
|
+
if(asked_on_another_screen(gobj, event, kw)) {
|
|
4160
|
+
return -1;
|
|
4161
|
+
}
|
|
4095
4162
|
if(kw.what === "import") {
|
|
4096
4163
|
return run_import(gobj);
|
|
4097
4164
|
}
|
|
@@ -1504,7 +1504,7 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
|
|
|
1504
1504
|
|
|
1505
1505
|
if(!priv.gobj_remote_yuno) {
|
|
1506
1506
|
log_error(`${gobj_short_name(gobj)}: No gobj_remote_yuno defined`);
|
|
1507
|
-
return;
|
|
1507
|
+
return -1;
|
|
1508
1508
|
}
|
|
1509
1509
|
|
|
1510
1510
|
let command = "update-node";
|
|
@@ -1528,7 +1528,9 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
|
|
|
1528
1528
|
);
|
|
1529
1529
|
if(ret) {
|
|
1530
1530
|
log_error(ret);
|
|
1531
|
+
return -1;
|
|
1531
1532
|
}
|
|
1533
|
+
return 0;
|
|
1532
1534
|
}
|
|
1533
1535
|
|
|
1534
1536
|
/************************************************************
|
|
@@ -2113,6 +2115,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2113
2115
|
* would tell a schema editor that the schema changed because
|
|
2114
2116
|
* somebody dragged a node.
|
|
2115
2117
|
*/
|
|
2118
|
+
if(result < 0 && command === "update-node") {
|
|
2119
|
+
graphs_write_refused(gobj,
|
|
2120
|
+
kw_get_str(gobj, kw_command, "topic_name", "", 0),
|
|
2121
|
+
kw_get_dict(gobj, kw_command, "record", {}, 0));
|
|
2122
|
+
}
|
|
2116
2123
|
if(result >= 0 &&
|
|
2117
2124
|
kw_get_str(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") {
|
|
2118
2125
|
gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
|
|
@@ -2473,22 +2480,62 @@ function ac_update_node(gobj, event, kw, src)
|
|
|
2473
2480
|
{
|
|
2474
2481
|
let priv = gobj.priv;
|
|
2475
2482
|
|
|
2476
|
-
if(refuse_if_readonly(gobj, event)) {
|
|
2477
|
-
return -1;
|
|
2478
|
-
}
|
|
2479
|
-
|
|
2480
2483
|
let treedb_name = priv.treedb_name;
|
|
2481
2484
|
let topic_name = kw.topic_name;
|
|
2482
2485
|
let record = kw.record;
|
|
2483
2486
|
let options = kw.options || {};
|
|
2484
2487
|
|
|
2485
|
-
|
|
2488
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2489
|
+
graphs_write_refused(gobj, topic_name, record);
|
|
2490
|
+
return -1;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
/* The engine's arrangement is sent with no way back when it lands,
|
|
2494
|
+
* so one that cannot leave must be said here: out of session the
|
|
2495
|
+
* transport may answer nothing at all. */
|
|
2496
|
+
if(topic_name === "__graphs__" && !is_connected(gobj)) {
|
|
2497
|
+
log_warning(`${gobj_short_name(gobj)}: no session, the arrangement ` +
|
|
2498
|
+
`was not sent`);
|
|
2499
|
+
graphs_write_refused(gobj, topic_name, record);
|
|
2500
|
+
return -1;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
let ret = treedb_update_node(
|
|
2486
2504
|
gobj,
|
|
2487
2505
|
treedb_name,
|
|
2488
2506
|
topic_name,
|
|
2489
2507
|
record,
|
|
2490
2508
|
options
|
|
2491
2509
|
);
|
|
2510
|
+
if(ret < 0) {
|
|
2511
|
+
graphs_write_refused(gobj, topic_name, record);
|
|
2512
|
+
}
|
|
2513
|
+
return ret;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
/************************************************************
|
|
2517
|
+
* A write of `__graphs__` did not land. The engine
|
|
2518
|
+
* (C_G6_NODES_TREE) took it for granted when it left -- nothing
|
|
2519
|
+
* answers it when it lands -- so it is told, and its next Save
|
|
2520
|
+
* writes the topic again. Other topics are the operator's data:
|
|
2521
|
+
* the answer's error says what happened to them.
|
|
2522
|
+
************************************************************/
|
|
2523
|
+
function graphs_write_refused(gobj, topic_name, record)
|
|
2524
|
+
{
|
|
2525
|
+
let priv = gobj.priv;
|
|
2526
|
+
|
|
2527
|
+
if(topic_name !== "__graphs__") {
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
let topic = (record && typeof record === "object") ? (record.topic || record.id) : "";
|
|
2531
|
+
if(!topic) {
|
|
2532
|
+
log_error(`${gobj_short_name(gobj)}: a refused __graphs__ write names no topic`);
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
if(!priv.gobj_nodes_tree) {
|
|
2536
|
+
return; /* no engine: nothing believes it was saved */
|
|
2537
|
+
}
|
|
2538
|
+
gobj_send_event(priv.gobj_nodes_tree, "EV_GRAPHS_WRITE_REFUSED", {topic: topic}, gobj);
|
|
2492
2539
|
}
|
|
2493
2540
|
|
|
2494
2541
|
/********************************************
|
|
@@ -1500,8 +1500,8 @@ function create_tabulator(gobj)
|
|
|
1500
1500
|
* what the ceiling costs is having to widen the column, or open
|
|
1501
1501
|
* the record, to read the end of it.
|
|
1502
1502
|
* `maxInitialWidth`, not `maxWidth`: the latter is a HARD ceiling
|
|
1503
|
-
* that neither the drag handle nor the double click could pass
|
|
1504
|
-
*
|
|
1503
|
+
* that neither the drag handle nor the double click could pass;
|
|
1504
|
+
* this one caps only the width the
|
|
1505
1505
|
* layout gives, and leaves the column to the reader. */
|
|
1506
1506
|
if(max_col_width > 0) {
|
|
1507
1507
|
colDef.maxInitialWidth = max_col_width;
|
|
@@ -2153,8 +2153,8 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
|
|
|
2153
2153
|
let items = treedb_hook_data_size(value);
|
|
2154
2154
|
|
|
2155
2155
|
if(items > 0) {
|
|
2156
|
-
/* DOM, not an HTML string: the row id is record data
|
|
2157
|
-
*
|
|
2156
|
+
/* DOM, not an HTML string: the row id is record data. A
|
|
2157
|
+
* language change re-runs
|
|
2158
2158
|
* setColumns(), which re-runs this. */
|
|
2159
2159
|
value = createElement2(hook_cell_spec(
|
|
2160
2160
|
row.id, col.id, items, t("show linked records"), "show linked records"
|
|
@@ -2250,7 +2250,7 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
|
|
|
2250
2250
|
/*
|
|
2251
2251
|
* A string is record data, and Tabulator would parse it as markup
|
|
2252
2252
|
* (innerHTML): `a<b` lost its text, a field holding an `<img onerror>`
|
|
2253
|
-
* ran it
|
|
2253
|
+
* ran it. Shown as a text node.
|
|
2254
2254
|
*/
|
|
2255
2255
|
if(typeof value === "string" && value !== "") {
|
|
2256
2256
|
value = cell_text(value);
|
|
@@ -4035,7 +4035,7 @@ function ac_new_row(gobj, event, kw, src)
|
|
|
4035
4035
|
* What crosses the confirmation is the rows' IDENTITY, never their
|
|
4036
4036
|
* position: the view applies EV_TREEDB_NODE_* of every writer while the
|
|
4037
4037
|
* dialog is open, and a position names whatever row sits there when the
|
|
4038
|
-
* person answers -- with force:true
|
|
4038
|
+
* person answers -- with force:true.
|
|
4039
4039
|
************************************************************/
|
|
4040
4040
|
function ac_delete_rows(gobj, event, kw, src)
|
|
4041
4041
|
{
|
|
@@ -4103,8 +4103,8 @@ function ac_confirmed(gobj, event, kw, src)
|
|
|
4103
4103
|
* them): never replaced by the row that took their place. */
|
|
4104
4104
|
log_error(`${gobj_short_name(gobj)}: ${event}: rows gone while ` +
|
|
4105
4105
|
`confirming the delete, not deleted: ${found.missing.join(", ")}`);
|
|
4106
|
-
/* The person who answered "yes" is told, not only the
|
|
4107
|
-
*
|
|
4106
|
+
/* The person who answered "yes" is told, not only the
|
|
4107
|
+
* log. */
|
|
4108
4108
|
yui_shell_show_error(yui_shell_of(gobj), "some records were gone before the delete", {t: t});
|
|
4109
4109
|
}
|
|
4110
4110
|
if(!found.rows.length) {
|
|
@@ -4374,8 +4374,8 @@ function ac_files_read(gobj, event, kw, src)
|
|
|
4374
4374
|
* open and BUSY, and the serial it waits for is taken here.
|
|
4375
4375
|
*
|
|
4376
4376
|
* A refused write used to close the form all the same and throw
|
|
4377
|
-
* away what was typed, although the README said it stayed open
|
|
4378
|
-
*
|
|
4377
|
+
* away what was typed, although the README said it stayed open:
|
|
4378
|
+
* the close was posted with the
|
|
4379
4379
|
* publish, before the backend had answered anything.
|
|
4380
4380
|
************************************************************/
|
|
4381
4381
|
function wait_for_write_answer(gobj)
|
|
@@ -188,7 +188,7 @@ function mt_start(gobj)
|
|
|
188
188
|
* (yui_shell_set_connection_state). A write in flight when it drops
|
|
189
189
|
* is never answered, and its form waited for ever in every host
|
|
190
190
|
* that did not forward EV_TRANSPORT_STATE -- wattyzer and yunovatios
|
|
191
|
-
* among them
|
|
191
|
+
* among them. The transport itself is
|
|
192
192
|
* not subscribed to: see ac_transport_state(). */
|
|
193
193
|
let shell = yui_shell_of(gobj);
|
|
194
194
|
if(shell) {
|
|
@@ -2476,8 +2476,8 @@ function ac_transport_state(gobj, event, kw, src)
|
|
|
2476
2476
|
|
|
2477
2477
|
/*
|
|
2478
2478
|
* A write in flight when the transport closes is never answered by
|
|
2479
|
-
* the backend: its form stayed open and busy for ever
|
|
2480
|
-
*
|
|
2479
|
+
* the backend: its form stayed open and busy for ever. Answered
|
|
2480
|
+
* refused: the form stays open on what
|
|
2481
2481
|
* was typed, and the person saves again when the session is back.
|
|
2482
2482
|
*/
|
|
2483
2483
|
if(!connected) {
|
|
@@ -2648,8 +2648,8 @@ function ac_create_record(gobj, event, kw, src)
|
|
|
2648
2648
|
|
|
2649
2649
|
/* `create_only`: +New makes a NEW record. With `create` alone a taken
|
|
2650
2650
|
* id was an update -- the record overwritten and, through autolink
|
|
2651
|
-
* with the selects empty, unlinked, answered "Node update!"
|
|
2652
|
-
*
|
|
2651
|
+
* with the selects empty, unlinked, answered "Node update!". A
|
|
2652
|
+
* backend older than the option ignores it
|
|
2653
2653
|
* and keeps the old upsert. */
|
|
2654
2654
|
let options = {
|
|
2655
2655
|
list_dict: true,
|
|
@@ -2895,8 +2895,8 @@ function ac_update_record(gobj, event, kw, src)
|
|
|
2895
2895
|
/************************************************************
|
|
2896
2896
|
* A write sent with no session never leaves: C_IEVENT_CLI logs
|
|
2897
2897
|
* "Not in session" and answers null, as it does when the command
|
|
2898
|
-
* went -- so a form waiting for the answer waited for ever
|
|
2899
|
-
*
|
|
2898
|
+
* went -- so a form waiting for the answer waited for ever. Refused
|
|
2899
|
+
* before it is sent, aloud.
|
|
2900
2900
|
************************************************************/
|
|
2901
2901
|
function refuse_without_session(gobj, topic_name)
|
|
2902
2902
|
{
|
package/src/form_busy.js
CHANGED
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
* that back. So a second busy(true) while already busy must not
|
|
11
11
|
* run at all -- it read the buttons it had just disabled as
|
|
12
12
|
* "disabled on their own", and the busy(false) of a refused write
|
|
13
|
-
* left Save and Cancel dead (
|
|
14
|
-
*
|
|
15
|
-
* the write). The caller owns `state` ({busy}) for the form's life.
|
|
13
|
+
* left Save and Cancel dead (a record with a picked file went
|
|
14
|
+
* busy for the read, then busy again for the write). The caller owns `state` ({busy}) for the form's life.
|
|
16
15
|
*
|
|
17
16
|
* Copyright (c) 2026, ArtGins.
|
|
18
17
|
* All Rights Reserved.
|
package/src/form_time_value.js
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* A Date -> the value of a `datetime-local` input, in local time,
|
|
38
38
|
* WITH seconds: the input is built with `step: 1`, which is what
|
|
39
39
|
* lets it hold them. Written as "YYYY-MM-DDTHH:mm" it lost them on
|
|
40
|
-
* every save of any field
|
|
40
|
+
* every save of any field.
|
|
41
41
|
************************************************************/
|
|
42
42
|
function date_to_datetime_local(value)
|
|
43
43
|
{
|
|
@@ -4,7 +4,7 @@ import {date_to_datetime_local, datetime_local_to_epoch} from "./form_time_value
|
|
|
4
4
|
describe("form_time_value", () => {
|
|
5
5
|
|
|
6
6
|
it("an epoch with seconds survives a round trip through the input", () => {
|
|
7
|
-
/*
|
|
7
|
+
/* The input was written without
|
|
8
8
|
* seconds, so every save of ANY field moved a writable time
|
|
9
9
|
* column back up to 59 s. */
|
|
10
10
|
const epoch = 1790000037; // :37 seconds
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* answers, so the host has to answer EVERY way a write ends --
|
|
8
8
|
* and the transport closing under a write in flight, or a
|
|
9
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).
|
|
10
|
+
* of them that nobody answered: the form stayed busy for ever.
|
|
12
11
|
*
|
|
13
12
|
* A write is known by its TOPIC and its `form_write` serial,
|
|
14
13
|
* never by the serial alone: each topic has its own form, and
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* g6_nodes_tree_fsm.test.js
|
|
3
|
+
*
|
|
4
|
+
* The machine of the real C_G6_NODES_TREE: the events its host
|
|
5
|
+
* sends it are declared, and each has an action. The host's own
|
|
6
|
+
* tests drive a fake engine, which cannot see this.
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) 2026, ArtGins.
|
|
9
|
+
* All Rights Reserved.
|
|
10
|
+
***********************************************************************/
|
|
11
|
+
import {describe, test, expect} from "vitest";
|
|
12
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
13
|
+
|
|
14
|
+
install_dom_double();
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
gclass_find_by_name, gclass_event_type, gclass_check_fsm,
|
|
18
|
+
} = await import("@yuneta/gobj-js");
|
|
19
|
+
const {register_c_g6_nodes_tree} = await import("./c_g6_nodes_tree.js");
|
|
20
|
+
|
|
21
|
+
describe("C_G6_NODES_TREE", () => {
|
|
22
|
+
|
|
23
|
+
test("hears EV_GRAPHS_WRITE_REFUSED, the host's word on a __graphs__ write", () => {
|
|
24
|
+
register_c_g6_nodes_tree();
|
|
25
|
+
const gclass = gclass_find_by_name("C_G6_NODES_TREE", false);
|
|
26
|
+
expect(gclass).toBeTruthy();
|
|
27
|
+
expect(gclass_event_type(gclass, "EV_GRAPHS_WRITE_REFUSED")).toBeTruthy();
|
|
28
|
+
const idle = gclass.dl_states.find((st) => st.state_name === "ST_IDLE");
|
|
29
|
+
expect(idle.dl_actions.some((a) => a.event_name === "EV_GRAPHS_WRITE_REFUSED" &&
|
|
30
|
+
typeof a.action === "function")).toBe(true);
|
|
31
|
+
expect(gclass_check_fsm(gclass)).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/graph_save_plan.js
CHANGED
|
@@ -140,7 +140,35 @@ function apply_graphs_echo(live, saved, records, rec)
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/***************************************************************
|
|
144
|
+
* A write of one topic's arrangement did NOT land: the backend
|
|
145
|
+
* refused it, the transport refused it on the way out, or there
|
|
146
|
+
* was no session to send it on.
|
|
147
|
+
*
|
|
148
|
+
* saved {topic_name: properties} what the backend has
|
|
149
|
+
* topic_name the topic of the refused record
|
|
150
|
+
*
|
|
151
|
+
* The view records a write as saved BEFORE it leaves, because
|
|
152
|
+
* nothing answers it when it lands. So a refused one left the view
|
|
153
|
+
* believing the backend held an arrangement it does not, and the
|
|
154
|
+
* next Save found nothing to write. What the backend holds for the
|
|
155
|
+
* topic is not known any more, so it is forgotten: the next Save
|
|
156
|
+
* writes the topic again (plan_graph_saves()).
|
|
157
|
+
*
|
|
158
|
+
* Returns TRUE if there was something to forget.
|
|
159
|
+
***************************************************************/
|
|
160
|
+
function forget_refused_graphs_write(saved, topic_name)
|
|
161
|
+
{
|
|
162
|
+
if(!is_object(saved) || !topic_name ||
|
|
163
|
+
!Object.prototype.hasOwnProperty.call(saved, topic_name)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
delete saved[topic_name];
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
143
170
|
export {
|
|
171
|
+
forget_refused_graphs_write,
|
|
144
172
|
apply_graphs_echo,
|
|
145
173
|
plan_graph_saves,
|
|
146
174
|
topic_arrangement_changed,
|