@yuneta/gobj-ui 7.25.14 → 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 +100 -4
- package/dist/gobj-ui.cjs.js +406 -94
- package/dist/gobj-ui.es.js +406 -94
- package/package.json +2 -2
- package/src/c_g6_nodes_tree.js +79 -15
- package/src/c_yui_schema_editor.js +343 -66
- 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_busy.test.js +1 -1
- 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/form_writes_in_flight.test.js +1 -1
- package/src/g6_graphs_write_owed.wiring.test.js +200 -0
- 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/host_drafts.test.js +2 -2
- package/src/schema_editor_drafts.wiring.test.js +2 -2
- package/src/schema_editor_reload.wiring.test.js +329 -49
- package/src/schema_editor_transport.wiring.test.js +2 -2
- package/src/schema_version_bump.test.js +1 -1
- package/src/shell_modals.js +31 -11
- package/src/shell_toast_once.test.js +73 -10
- package/src/table_cell_text.js +1 -1
- package/src/table_cell_text.test.js +1 -1
- package/src/treedb_graph_writes.wiring.test.js +208 -0
- package/src/treedb_topics_transport.wiring.test.js +2 -2
- package/src/yui_install.js +14 -3
- package/src/yui_install.test.js +23 -0
- package/src/yui_row_search.test.js +1 -1
- package/src/yui_table_select.test.js +1 -1
|
@@ -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_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
|
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
|
|
@@ -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
|
+
});
|
|
@@ -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,
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
***********************************************************************/
|
|
19
19
|
import {describe, test, expect} from "vitest";
|
|
20
20
|
import {
|
|
21
|
-
plan_graph_saves, topic_arrangement_changed, arrangement_of, apply_graphs_echo
|
|
21
|
+
plan_graph_saves, topic_arrangement_changed, arrangement_of, apply_graphs_echo,
|
|
22
|
+
forget_refused_graphs_write
|
|
22
23
|
} from "./graph_save_plan.js";
|
|
23
24
|
|
|
24
25
|
const ORIGIN = "f3163a9f-1f37-412a-b8c9-87a4341f14de";
|
|
@@ -150,7 +151,7 @@ describe("arrangement_of", () => {
|
|
|
150
151
|
* save by another browser, sends back. It used to rebuild the saved
|
|
151
152
|
* snapshot of EVERY topic, taking the copies from the live objects the
|
|
152
153
|
* view had already rearranged: what was unsaved in OTHER topics counted
|
|
153
|
-
* as saved, and the next Save skipped it
|
|
154
|
+
* as saved, and the next Save skipped it.
|
|
154
155
|
*/
|
|
155
156
|
describe("an echo of one __graphs__ record", () => {
|
|
156
157
|
function state()
|
|
@@ -200,3 +201,36 @@ describe("an echo of one __graphs__ record", () => {
|
|
|
200
201
|
expect(live.yunos).toBeDefined();
|
|
201
202
|
});
|
|
202
203
|
});
|
|
204
|
+
|
|
205
|
+
/*
|
|
206
|
+
* A write of `__graphs__` that the backend REFUSED. The view takes the
|
|
207
|
+
* write for granted before it leaves (nothing answers it when it lands),
|
|
208
|
+
* so a refused one was recorded as saved: the next Save found nothing to
|
|
209
|
+
* write, and the arrangement was never stored.
|
|
210
|
+
*/
|
|
211
|
+
describe("a refused __graphs__ write", () => {
|
|
212
|
+
|
|
213
|
+
test("is planned again by the next Save", () => {
|
|
214
|
+
let live = five_topics();
|
|
215
|
+
let saved = five_topics();
|
|
216
|
+
live.yunos.nodes["1"].x = 500;
|
|
217
|
+
expect(plan_graph_saves(live, saved)).toEqual(["yunos"]);
|
|
218
|
+
|
|
219
|
+
/* What save_topic_graph_properties() does before the publish. */
|
|
220
|
+
saved.yunos = JSON.parse(JSON.stringify(live.yunos));
|
|
221
|
+
expect(plan_graph_saves(live, saved)).toEqual([]);
|
|
222
|
+
|
|
223
|
+
expect(forget_refused_graphs_write(saved, "yunos")).toBe(true);
|
|
224
|
+
expect(plan_graph_saves(live, saved)).toEqual(["yunos"]);
|
|
225
|
+
expect(Object.keys(saved)).not.toContain("yunos");
|
|
226
|
+
expect(Object.keys(saved).length).toBe(4);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a topic the view never believed saved: nothing to forget", () => {
|
|
230
|
+
let saved = five_topics();
|
|
231
|
+
expect(forget_refused_graphs_write(saved, "nothing")).toBe(false);
|
|
232
|
+
expect(forget_refused_graphs_write(saved, "")).toBe(false);
|
|
233
|
+
expect(forget_refused_graphs_write(null, "yunos")).toBe(false);
|
|
234
|
+
expect(Object.keys(saved).length).toBe(5);
|
|
235
|
+
});
|
|
236
|
+
});
|
package/src/host_drafts.js
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* it, and that mark lived in the memory of the session only:
|
|
8
8
|
* a reload of the page, a reconnect or a refresh of the model
|
|
9
9
|
* lost the chip, the banner and the export warning while
|
|
10
|
-
* __system__ still differed from the file in use
|
|
11
|
-
*
|
|
12
|
-
* C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
|
|
10
|
+
* __system__ still differed from the file in use. The host
|
|
11
|
+
* reads that difference from C_TREEDB's `saved-schema` (`draft_changed`, by treedb) and
|
|
13
12
|
* hands it here; the editor applies it to its model, in place
|
|
14
13
|
* of what the host said before.
|
|
15
14
|
*
|
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. */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* EV_DRAFTS driven through a real C_YUI_SCHEMA_EDITOR on a document
|
|
5
5
|
* double. The helper was right and tested; the ACTION folded the
|
|
6
6
|
* host's marks into the session's, so a topic the host named before
|
|
7
|
-
* a Save stayed a draft after it
|
|
7
|
+
* a Save stayed a draft after it.
|
|
8
8
|
*
|
|
9
9
|
* Copyright (c) 2026, ArtGins.
|
|
10
10
|
* All Rights Reserved.
|
|
@@ -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");
|