@yuneta/gobj-ui 7.25.15 → 7.25.16
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 +59 -4
- package/dist/gobj-ui.cjs.js +222 -41
- package/dist/gobj-ui.es.js +222 -41
- package/package.json +2 -2
- package/src/c_g6_nodes_tree.js +37 -4
- package/src/c_yui_schema_editor.js +239 -29
- package/src/form_busy.test.js +1 -1
- package/src/form_writes_in_flight.test.js +1 -1
- package/src/g6_graphs_write_owed.wiring.test.js +200 -0
- package/src/host_drafts.test.js +2 -2
- package/src/schema_editor_drafts.wiring.test.js +1 -1
- package/src/schema_editor_reload.wiring.test.js +152 -4
- package/src/shell_modals.js +29 -8
- package/src/shell_toast_once.test.js +63 -0
- package/src/table_cell_text.test.js +1 -1
- package/src/treedb_topics_transport.wiring.test.js +1 -1
- package/src/yui_install.js +14 -3
- package/src/yui_install.test.js +23 -0
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
kw_get_int,
|
|
118
118
|
kw_get_dict,
|
|
119
119
|
is_object,
|
|
120
|
+
json_deep_copy,
|
|
120
121
|
is_array,
|
|
121
122
|
empty_string,
|
|
122
123
|
} from "@yuneta/gobj-js";
|
|
@@ -246,6 +247,8 @@ let PRIVATE_DATA = {
|
|
|
246
247
|
dropped_round: 0, /* the load a drop ended: its failures are the drop, said once */
|
|
247
248
|
dropped_write: 0, /* the same for the write a drop ended */
|
|
248
249
|
reload_after_write: false, /* a write given up turned out DONE while another was in flight */
|
|
250
|
+
refresh_after_write: false, /* EV_REFRESH heard while writes were in flight: run at their end */
|
|
251
|
+
reopen_form: null, /* the form whose Save ran the owed reload: opened again once it lands */
|
|
249
252
|
|
|
250
253
|
/* A position that arrived while a load or a write was in flight */
|
|
251
254
|
pending_seg: null,
|
|
@@ -522,12 +525,37 @@ function owes_reload(gobj)
|
|
|
522
525
|
state !== "ST_LOADING" && state !== "ST_SAVING";
|
|
523
526
|
}
|
|
524
527
|
|
|
525
|
-
function reload_first(gobj, event)
|
|
528
|
+
function reload_first(gobj, event, reopen)
|
|
526
529
|
{
|
|
530
|
+
let priv = gobj.priv;
|
|
531
|
+
|
|
527
532
|
if(!owes_reload(gobj)) {
|
|
528
533
|
return false;
|
|
529
534
|
}
|
|
530
|
-
|
|
535
|
+
priv.reload_on_open = false;
|
|
536
|
+
|
|
537
|
+
/* The Save of a FORM: what the operator typed is not thrown away
|
|
538
|
+
* with the model it was typed on. The load that lands closes the
|
|
539
|
+
* form (end_load()) and opens it again on the schemas it read,
|
|
540
|
+
* with the fields the operator CHANGED put back on top -- only
|
|
541
|
+
* those: a field left as it was shows what the store holds now,
|
|
542
|
+
* so the next Save does not write the old value over a newer
|
|
543
|
+
* one. Closed alone, the form took what was typed with it. */
|
|
544
|
+
if(reopen && priv.dialog) {
|
|
545
|
+
priv.reopen_form = Object.assign(json_deep_copy(reopen), {
|
|
546
|
+
dialog: priv.dialog,
|
|
547
|
+
treedb_id: priv.treedb_id
|
|
548
|
+
});
|
|
549
|
+
log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas shown ` +
|
|
550
|
+
`may be out of date: they are read again first, and the form opens again ` +
|
|
551
|
+
`on them with its changes`);
|
|
552
|
+
yui_shell_show_error(yui_shell_of(gobj),
|
|
553
|
+
"the schemas shown may be out of date: they are read again, and the form opens again on them with your changes",
|
|
554
|
+
{t: t});
|
|
555
|
+
request_model(gobj, true);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
531
559
|
log_warning(`${gobj_short_name(gobj)}: ${event} refused, the schemas shown ` +
|
|
532
560
|
`may be out of date: they are read again first`);
|
|
533
561
|
yui_shell_show_error(yui_shell_of(gobj),
|
|
@@ -537,6 +565,53 @@ function reload_first(gobj, event)
|
|
|
537
565
|
return true;
|
|
538
566
|
}
|
|
539
567
|
|
|
568
|
+
/***************************************************************
|
|
569
|
+
* The form reload_first() took down, opened again on the model
|
|
570
|
+
* the load just made, with the operator's changes on top.
|
|
571
|
+
*
|
|
572
|
+
* Only where it can mean the same thing: on the treedb it was
|
|
573
|
+
* opened on, on the screen whose Save it has, and on a record
|
|
574
|
+
* that is still there. Otherwise it is not opened, and that is
|
|
575
|
+
* said -- a form opened on another treedb, or on nothing, would
|
|
576
|
+
* save somewhere the operator never meant.
|
|
577
|
+
***************************************************************/
|
|
578
|
+
function reopen_form(gobj, r)
|
|
579
|
+
{
|
|
580
|
+
let priv = gobj.priv;
|
|
581
|
+
let state = gobj_current_state(gobj);
|
|
582
|
+
let treedb = current_treedb(gobj);
|
|
583
|
+
let topic = null;
|
|
584
|
+
let col = null;
|
|
585
|
+
let fits = false;
|
|
586
|
+
|
|
587
|
+
if(treedb && r.treedb_id === priv.treedb_id) {
|
|
588
|
+
if(r.form === "column" && state === "ST_COLUMNS") {
|
|
589
|
+
topic = find_topic(priv.model, priv.treedb_id, r.topic);
|
|
590
|
+
col = (topic && !r.creating) ?
|
|
591
|
+
find_col(priv.model, priv.treedb_id, r.topic, r.col) : null;
|
|
592
|
+
fits = !!topic && (r.creating || !!col);
|
|
593
|
+
} else if(r.form === "topic" && state === "ST_TOPICS") {
|
|
594
|
+
topic = r.creating ? null : find_topic(priv.model, priv.treedb_id, r.topic);
|
|
595
|
+
fits = r.creating || !!topic;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if(!fits) {
|
|
599
|
+
log_warning(`${gobj_short_name(gobj)}: the ${r.form} form was not opened again: ` +
|
|
600
|
+
`what it edited is not on the schemas the reload read ` +
|
|
601
|
+
`(${r.treedb_id} ${r.topic}${r.col ? "." + r.col : ""}, now ${state})`);
|
|
602
|
+
yui_shell_show_error(yui_shell_of(gobj),
|
|
603
|
+
"the schemas were read again and what the form was editing is not there any more",
|
|
604
|
+
{t: t});
|
|
605
|
+
return -1;
|
|
606
|
+
}
|
|
607
|
+
if(r.form === "column") {
|
|
608
|
+
open_column_form(gobj, topic, col, null, r.changed);
|
|
609
|
+
} else {
|
|
610
|
+
open_topic_form(gobj, treedb, topic, r.changed);
|
|
611
|
+
}
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
540
615
|
function reload_after_move(gobj)
|
|
541
616
|
{
|
|
542
617
|
if(!owes_reload(gobj)) {
|
|
@@ -725,7 +800,7 @@ function build_model(gobj)
|
|
|
725
800
|
* measurement of "changed since" can be measured against.
|
|
726
801
|
*
|
|
727
802
|
* `written` is forgotten only by an EV_REFRESH (the view's own
|
|
728
|
-
* Refresh button, or a host that sends one). Since
|
|
803
|
+
* Refresh button, or a host that sends one). Since 7.23.196 a write moves no
|
|
729
804
|
* version, so `written` is the draft chip of what this session
|
|
730
805
|
* wrote and no longer half of a version measurement: a reload of
|
|
731
806
|
* the reconnect, or of a write that turned out done, forgot the
|
|
@@ -1915,6 +1990,75 @@ function read_form($root)
|
|
|
1915
1990
|
return out;
|
|
1916
1991
|
}
|
|
1917
1992
|
|
|
1993
|
+
/***************************************************************
|
|
1994
|
+
* A column form as its Save sends it: the fields and the flags.
|
|
1995
|
+
***************************************************************/
|
|
1996
|
+
function column_form_state($form)
|
|
1997
|
+
{
|
|
1998
|
+
let values = read_form($form);
|
|
1999
|
+
values.flag = read_flags($form);
|
|
2000
|
+
return values;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
/***************************************************************
|
|
2004
|
+
* The fields the operator CHANGED: those whose value is not the
|
|
2005
|
+
* one the form was opened with.
|
|
2006
|
+
***************************************************************/
|
|
2007
|
+
function form_changes(initial, values)
|
|
2008
|
+
{
|
|
2009
|
+
let changed = {};
|
|
2010
|
+
|
|
2011
|
+
for(let name of Object.keys(values)) {
|
|
2012
|
+
if(JSON.stringify(values[name]) !== JSON.stringify(initial[name])) {
|
|
2013
|
+
changed[name] = values[name];
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return changed;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/***************************************************************
|
|
2020
|
+
* Put changes back into a form just built (reopen_form()). The
|
|
2021
|
+
* flags are drawn again for the type the changes leave, holding
|
|
2022
|
+
* the flags they name, so none of them is hidden by the type.
|
|
2023
|
+
***************************************************************/
|
|
2024
|
+
function apply_form_changes(gobj, $form, changes)
|
|
2025
|
+
{
|
|
2026
|
+
for(let name of Object.keys(changes)) {
|
|
2027
|
+
if(name === "flag") {
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
let $control = $form.querySelector(`[data-name="${name}"]`);
|
|
2031
|
+
if(!$control) {
|
|
2032
|
+
log_warning(`${gobj_short_name(gobj)}: a change of '${name}' has no field ` +
|
|
2033
|
+
`in the form opened again: not put back`);
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
if($control.type === "checkbox") {
|
|
2037
|
+
$control.checked = !!changes[name];
|
|
2038
|
+
} else {
|
|
2039
|
+
$control.value = changes[name];
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
let $type = $form.querySelector('[data-name="type"]');
|
|
2044
|
+
if($type && $form.querySelector(".SCHEMA_FLAGS")) {
|
|
2045
|
+
let flags = Array.isArray(changes.flag) ? changes.flag : read_flags($form);
|
|
2046
|
+
redraw_flags(gobj, $form, $type.value, flags);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/***************************************************************
|
|
2051
|
+
* Draw the flags of a column form again, for `type`, with
|
|
2052
|
+
* `current` checked.
|
|
2053
|
+
***************************************************************/
|
|
2054
|
+
function redraw_flags(gobj, $form, type, current)
|
|
2055
|
+
{
|
|
2056
|
+
let $holder = $form.querySelector(".SCHEMA_FLAGS");
|
|
2057
|
+
let $new = createElement2(flags_field(gobj, type, current));
|
|
2058
|
+
$holder.parentNode.replaceChild($new, $holder);
|
|
2059
|
+
refresh_language($new, t);
|
|
2060
|
+
}
|
|
2061
|
+
|
|
1918
2062
|
/***************************************************************
|
|
1919
2063
|
* The flags of a column, as checkboxes that say what they do.
|
|
1920
2064
|
*
|
|
@@ -1977,7 +2121,7 @@ function read_flags($root)
|
|
|
1977
2121
|
* Offering a field that silently does something else is worse
|
|
1978
2122
|
* than not offering it.
|
|
1979
2123
|
***************************************************************/
|
|
1980
|
-
function open_column_form(gobj, topic, col, prefill)
|
|
2124
|
+
function open_column_form(gobj, topic, col, prefill, changes)
|
|
1981
2125
|
{
|
|
1982
2126
|
let priv = gobj.priv;
|
|
1983
2127
|
/* A DUPLICATE is a creation whose fields start filled: the name must
|
|
@@ -2051,11 +2195,7 @@ function open_column_form(gobj, topic, col, prefill)
|
|
|
2051
2195
|
* new type does not use. */
|
|
2052
2196
|
let $type = $form.querySelector('[data-name="type"]');
|
|
2053
2197
|
$type.addEventListener("change", () => {
|
|
2054
|
-
|
|
2055
|
-
let current = read_flags($form);
|
|
2056
|
-
let $new = createElement2(flags_field(gobj, $type.value, current));
|
|
2057
|
-
$holder.parentNode.replaceChild($new, $holder);
|
|
2058
|
-
refresh_language($new, t);
|
|
2198
|
+
redraw_flags(gobj, $form, $type.value, read_flags($form));
|
|
2059
2199
|
});
|
|
2060
2200
|
|
|
2061
2201
|
/* A flag that excludes another is answered here and not on save,
|
|
@@ -2075,15 +2215,22 @@ function open_column_form(gobj, topic, col, prefill)
|
|
|
2075
2215
|
evt.stopPropagation();
|
|
2076
2216
|
close_dialog(gobj);
|
|
2077
2217
|
});
|
|
2218
|
+
/* What the form showed before the operator touched it: what
|
|
2219
|
+
* was CHANGED is measured against it (form_changes()). */
|
|
2220
|
+
let initial = column_form_state($form);
|
|
2221
|
+
if(changes) {
|
|
2222
|
+
apply_form_changes(gobj, $form, changes);
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2078
2225
|
$form.querySelector(".SCHEMA_COL_FORM_SAVE").addEventListener("click", (evt) => {
|
|
2079
2226
|
evt.stopPropagation();
|
|
2080
|
-
let values =
|
|
2081
|
-
values.flag = read_flags($form);
|
|
2227
|
+
let values = column_form_state($form);
|
|
2082
2228
|
gobj_send_event(gobj, "EV_SAVE_COLUMN", {
|
|
2083
2229
|
topic: topic.name,
|
|
2084
2230
|
col: creating ? "" : col.name,
|
|
2085
2231
|
creating: creating,
|
|
2086
2232
|
values: values,
|
|
2233
|
+
changed: creating ? values : form_changes(initial, values),
|
|
2087
2234
|
model_gen: model_gen
|
|
2088
2235
|
}, gobj);
|
|
2089
2236
|
});
|
|
@@ -2112,7 +2259,7 @@ function stringify_field(value)
|
|
|
2112
2259
|
* actually has: a pkey naming no column is a topic that does
|
|
2113
2260
|
* not open, and it is the first thing the validator looks for.
|
|
2114
2261
|
***************************************************************/
|
|
2115
|
-
function open_topic_form(gobj, treedb, topic)
|
|
2262
|
+
function open_topic_form(gobj, treedb, topic, changes)
|
|
2116
2263
|
{
|
|
2117
2264
|
let record = (topic && topic.record) || {};
|
|
2118
2265
|
let creating = !topic;
|
|
@@ -2171,12 +2318,19 @@ function open_topic_form(gobj, treedb, topic)
|
|
|
2171
2318
|
evt.stopPropagation();
|
|
2172
2319
|
close_dialog(gobj);
|
|
2173
2320
|
});
|
|
2321
|
+
let initial = read_form($form);
|
|
2322
|
+
if(changes) {
|
|
2323
|
+
apply_form_changes(gobj, $form, changes);
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2174
2326
|
$form.querySelector(".SCHEMA_TOPIC_FORM_SAVE").addEventListener("click", (evt) => {
|
|
2175
2327
|
evt.stopPropagation();
|
|
2328
|
+
let values = read_form($form);
|
|
2176
2329
|
gobj_send_event(gobj, "EV_SAVE_TOPIC", {
|
|
2177
2330
|
topic: creating ? "" : topic.name,
|
|
2178
2331
|
creating: creating,
|
|
2179
|
-
values:
|
|
2332
|
+
values: values,
|
|
2333
|
+
changed: creating ? values : form_changes(initial, values),
|
|
2180
2334
|
model_gen: model_gen
|
|
2181
2335
|
}, gobj);
|
|
2182
2336
|
});
|
|
@@ -2756,27 +2910,46 @@ function end_load(gobj)
|
|
|
2756
2910
|
{
|
|
2757
2911
|
let priv = gobj.priv;
|
|
2758
2912
|
let seg = priv.pending_seg;
|
|
2913
|
+
let reopen = priv.reopen_form;
|
|
2759
2914
|
|
|
2760
2915
|
priv.pending_seg = null;
|
|
2916
|
+
priv.reopen_form = null;
|
|
2761
2917
|
if(priv.dialog && priv.dialog_gen !== priv.model_gen) {
|
|
2762
|
-
|
|
2763
|
-
|
|
2918
|
+
/* The form whose Save asked for this load is opened again,
|
|
2919
|
+
* below, once the screen is there (reload_first()). Any other
|
|
2920
|
+
* dialog is closed, and that is said. */
|
|
2921
|
+
if(!reopen || reopen.dialog !== priv.dialog) {
|
|
2922
|
+
reopen = null;
|
|
2923
|
+
log_warning(`${gobj_short_name(gobj)}: a dialog was open on the schemas ` +
|
|
2924
|
+
`the load replaced: closed`);
|
|
2925
|
+
yui_shell_show_error(yui_shell_of(gobj),
|
|
2926
|
+
"the schemas were read again: open the dialog again", {t: t});
|
|
2927
|
+
}
|
|
2764
2928
|
close_dialog(gobj);
|
|
2765
|
-
|
|
2766
|
-
|
|
2929
|
+
} else {
|
|
2930
|
+
/* Closed meanwhile by the operator, or a load that replaced
|
|
2931
|
+
* nothing: the form is where it was, or where it was left. */
|
|
2932
|
+
reopen = null;
|
|
2767
2933
|
}
|
|
2934
|
+
|
|
2935
|
+
let ret = 0;
|
|
2768
2936
|
if(priv.model) {
|
|
2769
2937
|
if(seg !== null) {
|
|
2770
|
-
|
|
2938
|
+
ret = apply_seg(gobj, seg);
|
|
2939
|
+
} else {
|
|
2940
|
+
ret = go(gobj, priv.treedb_id, priv.topic_name, priv.diagram, false);
|
|
2941
|
+
}
|
|
2942
|
+
} else {
|
|
2943
|
+
if(seg !== null) {
|
|
2944
|
+
adopt_seg(gobj, seg);
|
|
2771
2945
|
}
|
|
2772
|
-
|
|
2946
|
+
gobj_change_state(gobj, "ST_IDLE");
|
|
2947
|
+
render(gobj);
|
|
2773
2948
|
}
|
|
2774
|
-
if(
|
|
2775
|
-
|
|
2949
|
+
if(reopen) {
|
|
2950
|
+
reopen_form(gobj, reopen);
|
|
2776
2951
|
}
|
|
2777
|
-
|
|
2778
|
-
render(gobj);
|
|
2779
|
-
return 0;
|
|
2952
|
+
return ret;
|
|
2780
2953
|
}
|
|
2781
2954
|
|
|
2782
2955
|
/***************************************************************
|
|
@@ -2819,7 +2992,17 @@ function end_writes(gobj, error)
|
|
|
2819
2992
|
render(gobj);
|
|
2820
2993
|
publish_check(gobj);
|
|
2821
2994
|
}
|
|
2822
|
-
|
|
2995
|
+
if(priv.refresh_after_write) {
|
|
2996
|
+
/* The EV_REFRESH heard while the writes were in flight
|
|
2997
|
+
* (ac_refresh_after_the_writes()), run now that none is. It
|
|
2998
|
+
* reads the store whole, so it also pays a reload a late
|
|
2999
|
+
* write owed. */
|
|
3000
|
+
priv.refresh_after_write = false;
|
|
3001
|
+
priv.reload_after_write = false;
|
|
3002
|
+
request_model(gobj, false);
|
|
3003
|
+
} else {
|
|
3004
|
+
reload_if_owed(gobj);
|
|
3005
|
+
}
|
|
2823
3006
|
return error ? -1 : 0;
|
|
2824
3007
|
}
|
|
2825
3008
|
|
|
@@ -3279,7 +3462,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
3279
3462
|
gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
|
|
3280
3463
|
treedb_name: gobj_read_str_attr(gobj, "treedb_name")
|
|
3281
3464
|
});
|
|
3282
|
-
|
|
3465
|
+
/* An EV_REFRESH heard during the writes is paid by this
|
|
3466
|
+
* load: it forgets the drafts, as the refresh would. */
|
|
3467
|
+
let refresh = priv.refresh_after_write;
|
|
3468
|
+
priv.refresh_after_write = false;
|
|
3469
|
+
request_model(gobj, !refresh);
|
|
3283
3470
|
return 0;
|
|
3284
3471
|
}
|
|
3285
3472
|
}
|
|
@@ -3475,6 +3662,25 @@ function ac_refresh(gobj, event, kw, src)
|
|
|
3475
3662
|
return request_model(gobj, false);
|
|
3476
3663
|
}
|
|
3477
3664
|
|
|
3665
|
+
/***************************************************************
|
|
3666
|
+
* EV_REFRESH while writes are in flight: the view's own Refresh
|
|
3667
|
+
* is busy then, so it is a host's (after a Save, say).
|
|
3668
|
+
*
|
|
3669
|
+
* Run when the writes END (end_writes()), not now. Run now, the
|
|
3670
|
+
* load replaced ST_SAVING and what was still queued was never
|
|
3671
|
+
* sent, with no word: nothing but the end of what is in flight
|
|
3672
|
+
* moves this view out of ST_LOADING or ST_SAVING.
|
|
3673
|
+
***************************************************************/
|
|
3674
|
+
function ac_refresh_after_the_writes(gobj, event, kw, src)
|
|
3675
|
+
{
|
|
3676
|
+
let priv = gobj.priv;
|
|
3677
|
+
|
|
3678
|
+
log_warning(`${gobj_short_name(gobj)}: ${event} while writes are in flight: ` +
|
|
3679
|
+
`the schemas are read again when the writes end`);
|
|
3680
|
+
priv.refresh_after_write = true;
|
|
3681
|
+
return 0;
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3478
3684
|
/***************************************************************
|
|
3479
3685
|
* What a DIALOG sends while the model is being read again: the
|
|
3480
3686
|
* Save of a form left open when the reload started, a
|
|
@@ -3717,7 +3923,8 @@ function ac_save_column(gobj, event, kw, src)
|
|
|
3717
3923
|
if(refuse_if_readonly(gobj, event)) {
|
|
3718
3924
|
return -1;
|
|
3719
3925
|
}
|
|
3720
|
-
if(reload_first(gobj, event
|
|
3926
|
+
if(reload_first(gobj, event, {form: "column", topic: kw.topic, col: kw.col,
|
|
3927
|
+
creating: !!kw.creating, changed: kw.changed || {}})) {
|
|
3721
3928
|
return -1;
|
|
3722
3929
|
}
|
|
3723
3930
|
if(built_on_a_replaced_model(gobj, event, kw)) {
|
|
@@ -3892,7 +4099,8 @@ function ac_save_topic(gobj, event, kw, src)
|
|
|
3892
4099
|
if(refuse_if_readonly(gobj, event)) {
|
|
3893
4100
|
return -1;
|
|
3894
4101
|
}
|
|
3895
|
-
if(reload_first(gobj, event
|
|
4102
|
+
if(reload_first(gobj, event, {form: "topic", topic: kw.topic, col: "",
|
|
4103
|
+
creating: !!kw.creating, changed: kw.changed || {}})) {
|
|
3896
4104
|
return -1;
|
|
3897
4105
|
}
|
|
3898
4106
|
if(built_on_a_replaced_model(gobj, event, kw)) {
|
|
@@ -4306,7 +4514,9 @@ function create_gclass(gclass_name)
|
|
|
4306
4514
|
* the host are heard: every other action would be computed
|
|
4307
4515
|
* against a model the answer still in the air is about to
|
|
4308
4516
|
* change. */
|
|
4309
|
-
["ST_SAVING", COMMON.
|
|
4517
|
+
["ST_SAVING", COMMON.filter((a) => a[0] !== "EV_REFRESH").concat([
|
|
4518
|
+
["EV_REFRESH", ac_refresh_after_the_writes, null]
|
|
4519
|
+
])]
|
|
4310
4520
|
];
|
|
4311
4521
|
|
|
4312
4522
|
/*---------------------------------------------*
|
package/src/form_busy.test.js
CHANGED
|
@@ -53,7 +53,7 @@ describe("set_toolbar_busy", () => {
|
|
|
53
53
|
expect(tb.save.classList.contains("is-loading")).toBe(false);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
it("busy twice then free: Save and Cancel come back
|
|
56
|
+
it("busy twice then free: Save and Cancel come back", () => {
|
|
57
57
|
/* A record with a picked file went busy for the read and busy
|
|
58
58
|
* again for the write; the second pass recorded the buttons the
|
|
59
59
|
* first had disabled as disabled on their own, and the free of a
|
|
@@ -30,7 +30,7 @@ describe("form writes in flight", () => {
|
|
|
30
30
|
]);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it("the transport closes: every write in flight is abandoned, once
|
|
33
|
+
it("the transport closes: every write in flight is abandoned, once", () => {
|
|
34
34
|
/* The form of each one is answered EV_WRITE_REFUSED by the host
|
|
35
35
|
* and stays open on what was typed; before, it stayed busy for
|
|
36
36
|
* ever, because nobody answered a write whose answer was lost. */
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* g6_graphs_write_owed.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* A `__graphs__` write that did not land is OWED, and the Save
|
|
5
|
+
* button says so until a Save writes it again.
|
|
6
|
+
*
|
|
7
|
+
* EV_GRAPHS_WRITE_REFUSED lit the Save (in edition only), and
|
|
8
|
+
* the next repaint of the history buttons put it out again:
|
|
9
|
+
* update_history_buttons() decides the Save from
|
|
10
|
+
* `history.canUndo()` alone, and it runs on every history
|
|
11
|
+
* change, every change of mode and every redraw of the theme.
|
|
12
|
+
* A refusal heard in reading was never shown at all.
|
|
13
|
+
*
|
|
14
|
+
* The real C_G6_NODES_TREE, on a fake G6 graph: only the history
|
|
15
|
+
* plugin and the Save button are what is looked at.
|
|
16
|
+
*
|
|
17
|
+
* Copyright (c) 2026, ArtGins.
|
|
18
|
+
* All Rights Reserved.
|
|
19
|
+
***********************************************************************/
|
|
20
|
+
import {describe, test, expect, beforeAll, beforeEach} from "vitest";
|
|
21
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
22
|
+
|
|
23
|
+
install_dom_double();
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
SDATA, SDATA_END, data_type_t,
|
|
27
|
+
gclass_create, gobj_start_up, gobj_create_yuno, gobj_create_pure_child,
|
|
28
|
+
gobj_send_event, register_c_timer, set_log_callback,
|
|
29
|
+
} = await import("@yuneta/gobj-js");
|
|
30
|
+
const {register_c_g6_nodes_tree} = await import("./c_g6_nodes_tree.js");
|
|
31
|
+
|
|
32
|
+
const logged = [];
|
|
33
|
+
const published = [];
|
|
34
|
+
|
|
35
|
+
/* G6, as far as these paths ask it: plugins by key, and nothing on
|
|
36
|
+
* screen. Any other method answers undefined. */
|
|
37
|
+
function fake_graph(history)
|
|
38
|
+
{
|
|
39
|
+
let plugins = [];
|
|
40
|
+
const known = {
|
|
41
|
+
rendered: true,
|
|
42
|
+
getPlugins: () => plugins,
|
|
43
|
+
setPlugins: (fn) => {
|
|
44
|
+
plugins = (typeof fn === "function") ? fn(plugins) : fn;
|
|
45
|
+
},
|
|
46
|
+
getPluginInstance: (key) => (key === "history") ? history : {on() {}, off() {}},
|
|
47
|
+
updatePlugin: () => {},
|
|
48
|
+
getBehaviors: () => [],
|
|
49
|
+
setBehaviors: () => {},
|
|
50
|
+
getData: () => ({nodes: [], edges: [], combos: []}),
|
|
51
|
+
getZoom: () => 1,
|
|
52
|
+
};
|
|
53
|
+
return new Proxy(known, {
|
|
54
|
+
get: (t, k) => (k in t) ? t[k] : (() => undefined)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let yuno = null;
|
|
59
|
+
|
|
60
|
+
beforeAll(() => {
|
|
61
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
62
|
+
set_log_callback((level, msg) => {
|
|
63
|
+
logged.push({level: String(level), msg: String(msg)});
|
|
64
|
+
});
|
|
65
|
+
register_c_timer();
|
|
66
|
+
register_c_g6_nodes_tree();
|
|
67
|
+
/* The host: what the tree publishes lands here. */
|
|
68
|
+
const host_events = ["EV_UPDATE_NODE"];
|
|
69
|
+
gclass_create(
|
|
70
|
+
"C_TEST_GRAPH_HOST",
|
|
71
|
+
host_events.map((e) => [e, 0]),
|
|
72
|
+
[["ST_IDLE", host_events.map((e) => [e, (gobj, ev, kw) => {
|
|
73
|
+
published.push({event: ev, kw: kw});
|
|
74
|
+
return 0;
|
|
75
|
+
}, null])]],
|
|
76
|
+
{}, 0,
|
|
77
|
+
[SDATA(data_type_t.DTP_STRING, "node_uuid", 0, "test-node-uuid", ""), SDATA_END()],
|
|
78
|
+
{}, 0, 0, 0, 0
|
|
79
|
+
);
|
|
80
|
+
yuno = gobj_create_yuno("owed_yuno", "C_TEST_GRAPH_HOST", {});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
logged.length = 0;
|
|
85
|
+
published.length = 0;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let seq = 0;
|
|
89
|
+
|
|
90
|
+
/* A tree whose `users` arrangement the backend holds, in `mode`,
|
|
91
|
+
* with a Save button in its container and a history that has
|
|
92
|
+
* nothing to undo. */
|
|
93
|
+
function build(mode)
|
|
94
|
+
{
|
|
95
|
+
const $container = document.createElement("div");
|
|
96
|
+
const tree = gobj_create_pure_child(`tree_${++seq}`, "C_G6_NODES_TREE",
|
|
97
|
+
{$container: $container}, yuno);
|
|
98
|
+
const listeners = [];
|
|
99
|
+
const history = {
|
|
100
|
+
undo: false,
|
|
101
|
+
canUndo() {
|
|
102
|
+
return this.undo;
|
|
103
|
+
},
|
|
104
|
+
canRedo: () => false,
|
|
105
|
+
on: (ev, fn) => {
|
|
106
|
+
listeners.push(fn);
|
|
107
|
+
},
|
|
108
|
+
clear() {
|
|
109
|
+
this.undo = false;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
tree.priv.graph = fake_graph(history);
|
|
113
|
+
tree.priv.graph_rendered = true;
|
|
114
|
+
tree.priv.treedb_name = "treedb_x";
|
|
115
|
+
tree.priv._graph_properties = {users: {nodes: {u1: {x: 10, y: 20}}}};
|
|
116
|
+
tree.priv._saved_graph_properties = {users: {nodes: {u1: {x: 10, y: 20}}}};
|
|
117
|
+
|
|
118
|
+
const $save = document.createElement("div");
|
|
119
|
+
$save.className = "EV_SAVE_GRAPH";
|
|
120
|
+
$save.setAttribute("disabled", "");
|
|
121
|
+
$container.appendChild($save);
|
|
122
|
+
|
|
123
|
+
gobj_send_event(tree, "EV_SET_OPERATION_MODE", {operation_mode: mode}, tree);
|
|
124
|
+
return {
|
|
125
|
+
tree,
|
|
126
|
+
lit: () => !$save.hasAttribute("disabled"),
|
|
127
|
+
history_changed: () => {
|
|
128
|
+
for(const fn of listeners) {
|
|
129
|
+
fn();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function errors()
|
|
136
|
+
{
|
|
137
|
+
return logged.filter((l) => l.level === "error").map((l) => l.msg);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe("a refused __graphs__ write keeps the Save lit", () => {
|
|
141
|
+
|
|
142
|
+
test("in edition: a history change does not put it out", () => {
|
|
143
|
+
const g = build("edition");
|
|
144
|
+
expect(g.lit()).toBe(false);
|
|
145
|
+
|
|
146
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
147
|
+
expect(g.lit()).toBe(true);
|
|
148
|
+
|
|
149
|
+
g.history_changed(); /* nothing to undo */
|
|
150
|
+
expect(g.lit()).toBe(true);
|
|
151
|
+
|
|
152
|
+
gobj_send_event(g.tree, "EV_SET_OPERATION_MODE", {operation_mode: "edition"}, g.tree);
|
|
153
|
+
expect(g.lit()).toBe(true);
|
|
154
|
+
expect(errors()).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("heard in reading, it is shown on entering edition", () => {
|
|
158
|
+
const g = build("reading");
|
|
159
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
160
|
+
|
|
161
|
+
gobj_send_event(g.tree, "EV_SET_OPERATION_MODE", {operation_mode: "edition"}, g.tree);
|
|
162
|
+
expect(g.lit()).toBe(true);
|
|
163
|
+
expect(errors()).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("the Save writes it again, and then the Save goes dark", () => {
|
|
167
|
+
const g = build("edition");
|
|
168
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
169
|
+
|
|
170
|
+
gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
|
|
171
|
+
const writes = published.filter((p) => p.event === "EV_UPDATE_NODE");
|
|
172
|
+
expect(writes.map((w) => w.kw.record.id)).toEqual(["users"]);
|
|
173
|
+
expect(g.lit()).toBe(false);
|
|
174
|
+
|
|
175
|
+
g.history_changed();
|
|
176
|
+
expect(g.lit()).toBe(false);
|
|
177
|
+
expect(errors()).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("refused again after that Save: lit again", () => {
|
|
181
|
+
const g = build("edition");
|
|
182
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
183
|
+
gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
|
|
184
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
185
|
+
|
|
186
|
+
g.history_changed();
|
|
187
|
+
expect(g.lit()).toBe(true);
|
|
188
|
+
expect(errors()).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a reload forgets it: the view holds what the backend holds", () => {
|
|
192
|
+
const g = build("edition");
|
|
193
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
194
|
+
gobj_send_event(g.tree, "EV_CLEAR_DATA", {}, g.tree);
|
|
195
|
+
/* What the load does once the data is in again. */
|
|
196
|
+
gobj_send_event(g.tree, "EV_SET_OPERATION_MODE", {operation_mode: "edition"}, g.tree);
|
|
197
|
+
expect(g.lit()).toBe(false);
|
|
198
|
+
expect(errors()).toEqual([]);
|
|
199
|
+
});
|
|
200
|
+
});
|
package/src/host_drafts.test.js
CHANGED
|
@@ -15,7 +15,7 @@ const model = {
|
|
|
15
15
|
|
|
16
16
|
describe("mark_host_drafts", () => {
|
|
17
17
|
|
|
18
|
-
it("marks the topics the host names, by treedb
|
|
18
|
+
it("marks the topics the host names, by treedb", () => {
|
|
19
19
|
/* The mark lived in the memory of one session: after a reload the
|
|
20
20
|
* editor showed no draft while __system__ still differed from the
|
|
21
21
|
* file in use. Rebuilt from the host's data. */
|
|
@@ -68,7 +68,7 @@ describe("host_draft_ids", () => {
|
|
|
68
68
|
.toEqual({"treedb_a^users": true, "treedb_b^users": true});
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
it("is a fresh map every time: what the host stopped naming is gone
|
|
71
|
+
it("is a fresh map every time: what the host stopped naming is gone", () => {
|
|
72
72
|
/* mark_host_drafts() only ADDS, and the editor fed it the same map
|
|
73
73
|
* every time: a topic the host named before the Save stayed a
|
|
74
74
|
* draft after it. */
|
|
@@ -138,7 +138,7 @@ describe("EV_DRAFTS: the host is the truth", () => {
|
|
|
138
138
|
expect(drafts_on_screen(editor)).toBe(1);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
test("the host names none any more (a Save): the mark goes
|
|
141
|
+
test("the host names none any more (a Save): the mark goes", () => {
|
|
142
142
|
/* It only ever ADDED: after the Save the editor still said
|
|
143
143
|
* "unsaved schema changes". */
|
|
144
144
|
const {editor, host} = build("d2");
|