@yuneta/gobj-ui 7.25.17 → 7.25.19
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 +69 -3
- package/dist/gobj-ui.cjs.js +126 -30
- package/dist/gobj-ui.es.js +126 -30
- package/package.json +2 -2
- package/src/c_g6_nodes_tree.js +23 -0
- package/src/c_yui_node.js +71 -0
- package/src/c_yui_schema_editor.js +20 -10
- package/src/c_yui_treedb_graph.js +49 -6
- package/src/c_yui_treedb_topic_with_form.js +23 -0
- package/src/c_yui_treedb_topics.js +43 -9
- package/src/c_yui_uplot.js +10 -5
- package/src/form_json_expand.wiring.test.js +121 -0
- package/src/g6_graphs_write_owed.wiring.test.js +37 -0
- package/src/node_nav_events.wiring.test.js +150 -0
- package/src/schema_editor_reload.wiring.test.js +37 -0
- package/src/treedb_graph_writes.wiring.test.js +71 -3
- package/src/treedb_topics_transport.wiring.test.js +111 -7
- package/src/uplot_add_serie.test.js +92 -0
|
@@ -3696,6 +3696,27 @@ function refuse_if_readonly(gobj, event)
|
|
|
3696
3696
|
/************************************************************
|
|
3697
3697
|
* From external, at the beginning, load all topic data
|
|
3698
3698
|
************************************************************/
|
|
3699
|
+
/***************************************************************
|
|
3700
|
+
* One of MY json viewers (the schema, a cell, the table's records)
|
|
3701
|
+
* met a `__collapsed__` sentinel and the reader opened it. The
|
|
3702
|
+
* viewer never fetches: it asks its host, and waits on "loading"
|
|
3703
|
+
* until it is answered. This view holds records, not a store it can
|
|
3704
|
+
* read a subtree of -- the lazy drill is `print-tranger`, which the
|
|
3705
|
+
* TOPICS view runs for its own viewer, over a whole tranger -- so the
|
|
3706
|
+
* answer is that it cannot, and the viewer shows that on the stub
|
|
3707
|
+
* instead of loading for ever.
|
|
3708
|
+
***************************************************************/
|
|
3709
|
+
function ac_expand_path(gobj, event, kw, src)
|
|
3710
|
+
{
|
|
3711
|
+
let path = (kw && kw.path) || "";
|
|
3712
|
+
|
|
3713
|
+
gobj_send_event(src, "EV_SUBTREE_ERROR", {
|
|
3714
|
+
path: path,
|
|
3715
|
+
error: t("this part cannot be loaded here")
|
|
3716
|
+
}, gobj);
|
|
3717
|
+
return 0;
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3699
3720
|
/***************************************************************
|
|
3700
3721
|
* The language changed (the shell publishes it after the app switched).
|
|
3701
3722
|
*
|
|
@@ -5343,6 +5364,7 @@ function create_gclass(gclass_name)
|
|
|
5343
5364
|
["EV_CHANGE_LOCALE", ac_change_locale, null],
|
|
5344
5365
|
["EV_REFRESH", ac_refresh, null],
|
|
5345
5366
|
["EV_SHOW_SCHEMA", ac_show_schema, null],
|
|
5367
|
+
["EV_EXPAND_PATH", ac_expand_path, null],
|
|
5346
5368
|
["EV_CELL_EDITED", ac_cell_edited, null],
|
|
5347
5369
|
["EV_REPULL_PAGE", ac_repull_page, null],
|
|
5348
5370
|
["EV_PAGE_LOADED", ac_page_loaded, null],
|
|
@@ -5400,6 +5422,7 @@ function create_gclass(gclass_name)
|
|
|
5400
5422
|
["EV_CHANGE_LOCALE", 0],
|
|
5401
5423
|
["EV_REFRESH", 0],
|
|
5402
5424
|
["EV_SHOW_SCHEMA", 0],
|
|
5425
|
+
["EV_EXPAND_PATH", 0],
|
|
5403
5426
|
["EV_CELL_EDITED", 0],
|
|
5404
5427
|
["EV_REPULL_PAGE", 0],
|
|
5405
5428
|
["EV_PAGE_LOADED", 0],
|
|
@@ -1624,6 +1624,23 @@ function treedb_nodes(gobj, treedb_name, topic_name, options, page, purpose)
|
|
|
1624
1624
|
return 0;
|
|
1625
1625
|
}
|
|
1626
1626
|
|
|
1627
|
+
/************************************************************
|
|
1628
|
+
* The record a write answered: the node the store wrote (an
|
|
1629
|
+
* update or a create), deleted, or the child of a link. The
|
|
1630
|
+
* answer's frame carries back only `__md_command__`, never the
|
|
1631
|
+
* request's own record.
|
|
1632
|
+
************************************************************/
|
|
1633
|
+
function answered_record(data)
|
|
1634
|
+
{
|
|
1635
|
+
if(data && typeof data === "object" && !Array.isArray(data)) {
|
|
1636
|
+
return data;
|
|
1637
|
+
}
|
|
1638
|
+
if(Array.isArray(data) && data[0] && typeof data[0] === "object") {
|
|
1639
|
+
return data[0];
|
|
1640
|
+
}
|
|
1641
|
+
return {};
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1627
1644
|
/************************************************************
|
|
1628
1645
|
* Command to remote service
|
|
1629
1646
|
************************************************************/
|
|
@@ -1659,7 +1676,7 @@ function treedb_create_node(gobj, treedb_name, topic_name, record, options)
|
|
|
1659
1676
|
/************************************************************
|
|
1660
1677
|
* Command to remote service
|
|
1661
1678
|
************************************************************/
|
|
1662
|
-
function treedb_update_node(gobj, treedb_name, topic_name, record, options, form_write)
|
|
1679
|
+
function treedb_update_node(gobj, treedb_name, topic_name, record, options, form_write, created)
|
|
1663
1680
|
{
|
|
1664
1681
|
let command = "update-node";
|
|
1665
1682
|
|
|
@@ -1678,6 +1695,11 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options, form
|
|
|
1678
1695
|
if(form_write) {
|
|
1679
1696
|
kw.__md_command__.form_write = form_write;
|
|
1680
1697
|
}
|
|
1698
|
+
if(created) {
|
|
1699
|
+
/* A create goes out as an update-node (create_only); the answer
|
|
1700
|
+
* says `created` only if the request says it back. */
|
|
1701
|
+
kw.__md_command__.created = true;
|
|
1702
|
+
}
|
|
1681
1703
|
// TODO review msg_iev_write_key(kw, "__topic_name__", topic_name);
|
|
1682
1704
|
|
|
1683
1705
|
let ret = gobj_command(
|
|
@@ -1723,7 +1745,9 @@ function treedb_delete_node(gobj, treedb_name, topic_name, record, options)
|
|
|
1723
1745
|
);
|
|
1724
1746
|
if(ret) {
|
|
1725
1747
|
log_error(ret);
|
|
1748
|
+
return -1;
|
|
1726
1749
|
}
|
|
1750
|
+
return 0;
|
|
1727
1751
|
}
|
|
1728
1752
|
|
|
1729
1753
|
/************************************************************
|
|
@@ -2200,6 +2224,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2200
2224
|
|
|
2201
2225
|
case "create-node":
|
|
2202
2226
|
case "update-node":
|
|
2227
|
+
case "delete-node":
|
|
2203
2228
|
/*
|
|
2204
2229
|
* The view refreshes itself from the treedb's own
|
|
2205
2230
|
* EV_TREEDB_NODE_* events, which arrive for EVERY writer. This
|
|
@@ -2210,13 +2235,26 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2210
2235
|
* and cannot use the node events without answering its own writes
|
|
2211
2236
|
* in a loop.
|
|
2212
2237
|
*/
|
|
2238
|
+
/* From what THIS view knows and from the answer, never from
|
|
2239
|
+
* the request: the frame carries back only `__md_command__`,
|
|
2240
|
+
* so `treedb_name` and `record` read there arrived empty.
|
|
2241
|
+
* `record` is the node the store answered, as it was written
|
|
2242
|
+
* (for a delete, the node deleted). The table's own +New goes
|
|
2243
|
+
* out as an update-node, so `created` is also what the request
|
|
2244
|
+
* echoes back in `__md_command__`. */
|
|
2213
2245
|
gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
|
|
2214
|
-
treedb_name:
|
|
2246
|
+
treedb_name: gobj_read_str_attr(gobj, "treedb_name"),
|
|
2215
2247
|
topic_name: kw_get_str(gobj, kw_command, "topic_name", "", 0),
|
|
2216
|
-
record:
|
|
2217
|
-
created: (command === "create-node")
|
|
2248
|
+
record: answered_record(data),
|
|
2249
|
+
created: (command === "create-node") ||
|
|
2250
|
+
kw_command.created === true,
|
|
2218
2251
|
command: command
|
|
2219
2252
|
});
|
|
2253
|
+
if(command === "delete-node") {
|
|
2254
|
+
/* The rows go away on the treedb's own EV_TREEDB_NODE_DELETED;
|
|
2255
|
+
* a delete has no form waiting for its answer. */
|
|
2256
|
+
break;
|
|
2257
|
+
}
|
|
2220
2258
|
settle_form_write(gobj.priv.form_writes,
|
|
2221
2259
|
kw_get_int(gobj, kw_command, "form_write", 0, 0),
|
|
2222
2260
|
kw_get_str(gobj, kw_command, "topic_name", "", 0));
|
|
@@ -2228,10 +2266,6 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
2228
2266
|
);
|
|
2229
2267
|
break;
|
|
2230
2268
|
|
|
2231
|
-
case "delete-node":
|
|
2232
|
-
// Don't process by here, process on subscribed events.
|
|
2233
|
-
break;
|
|
2234
|
-
|
|
2235
2269
|
default:
|
|
2236
2270
|
log_error(`${gobj_short_name(gobj)} Command unknown: ${command}`);
|
|
2237
2271
|
}
|
|
@@ -2659,7 +2693,7 @@ function ac_create_record(gobj, event, kw, src)
|
|
|
2659
2693
|
};
|
|
2660
2694
|
|
|
2661
2695
|
if(treedb_update_node( // HACK use the powerful update_node
|
|
2662
|
-
gobj, treedb_name, topic_name, record, options, form_write) < 0) {
|
|
2696
|
+
gobj, treedb_name, topic_name, record, options, form_write, true) < 0) {
|
|
2663
2697
|
answer_form_write(gobj, src, form_write, false);
|
|
2664
2698
|
return -1; /* Error already logged */
|
|
2665
2699
|
}
|
package/src/c_yui_uplot.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Graphics, Charts with uPlot
|
|
5
5
|
*
|
|
6
|
-
* Copyright (c) 2025, ArtGins.
|
|
6
|
+
* Copyright (c) 2025-2026, ArtGins.
|
|
7
7
|
* All Rights Reserved.
|
|
8
8
|
***********************************************************************/
|
|
9
9
|
|
|
@@ -12,14 +12,12 @@ import {
|
|
|
12
12
|
SDATA_END,
|
|
13
13
|
data_type_t,
|
|
14
14
|
event_flag_t,
|
|
15
|
-
kw_flag_t,
|
|
16
15
|
gclass_create,
|
|
17
16
|
createElement2,
|
|
18
17
|
log_error,
|
|
19
18
|
gobj_read_pointer_attr,
|
|
20
19
|
gobj_parent,
|
|
21
20
|
gobj_subscribe_event,
|
|
22
|
-
kw_get_str,
|
|
23
21
|
gobj_read_attr,
|
|
24
22
|
gobj_read_str_attr,
|
|
25
23
|
gobj_write_attr,
|
|
@@ -457,8 +455,15 @@ function ac_add_serie(gobj, event, kw, src)
|
|
|
457
455
|
* colour: the old fallback was `Orange`, 1.97:1 on a white page,
|
|
458
456
|
* and it was what every series past the fourth got. */
|
|
459
457
|
let color = colors[Math.max(0, idx - 1) % colors.length];
|
|
460
|
-
|
|
461
|
-
|
|
458
|
+
/* Only when the host gave none: uPlot takes a stroke or a fill
|
|
459
|
+
* that is a FUNCTION (or a gradient) too, and kw_get_str() logs
|
|
460
|
+
* a value that is not a string (gobj-js 7.25.5, as C does). */
|
|
461
|
+
if(kw.stroke === undefined) {
|
|
462
|
+
kw.stroke = color.stroke;
|
|
463
|
+
}
|
|
464
|
+
if(kw.fill === undefined) {
|
|
465
|
+
kw.fill = color.fill;
|
|
466
|
+
}
|
|
462
467
|
//kw["paths"] = beziercurve_path; //rectangle_path;
|
|
463
468
|
uplot.addSeries(kw);
|
|
464
469
|
return 0;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* form_json_expand.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* C_YUI_TREEDB_TOPIC_WITH_FORM hosts C_YUI_JSON as a pure CHILD
|
|
5
|
+
* (the schema, a cell, the table's records), so it hears every
|
|
6
|
+
* event the viewer publishes -- and it did not declare
|
|
7
|
+
* EV_EXPAND_PATH, which the viewer publishes when the reader opens
|
|
8
|
+
* a `__collapsed__` sentinel. The click answered "Event NOT DEFINED
|
|
9
|
+
* in state" and the stub stayed on "loading" for good. The form
|
|
10
|
+
* cannot read a subtree (that is print-tranger, which the TOPICS
|
|
11
|
+
* view runs for its own viewer), so it says so, and the stub shows
|
|
12
|
+
* it.
|
|
13
|
+
*
|
|
14
|
+
* Driven through the real gclasses (C_YUI_SHELL,
|
|
15
|
+
* C_YUI_TREEDB_TOPIC_WITH_FORM, C_YUI_JSON) on a document double:
|
|
16
|
+
* the event is published by the real viewer the form created.
|
|
17
|
+
*
|
|
18
|
+
* Copyright (c) 2026, ArtGins.
|
|
19
|
+
* All Rights Reserved.
|
|
20
|
+
***********************************************************************/
|
|
21
|
+
import {describe, test, expect, beforeAll, beforeEach} from "vitest";
|
|
22
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
23
|
+
|
|
24
|
+
install_dom_double();
|
|
25
|
+
|
|
26
|
+
/* A phone: the viewer opens in the shell's sheet, not in a
|
|
27
|
+
* C_YUI_WINDOW, whose markup the document double cannot parse. */
|
|
28
|
+
globalThis.innerWidth = 400;
|
|
29
|
+
if(globalThis.window) {
|
|
30
|
+
globalThis.window.innerWidth = 400;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Tabulator watches its table; the document double has no observer. */
|
|
34
|
+
if(typeof globalThis.ResizeObserver === "undefined") {
|
|
35
|
+
globalThis.ResizeObserver = class {
|
|
36
|
+
observe() {}
|
|
37
|
+
unobserve() {}
|
|
38
|
+
disconnect() {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
SDATA_END, gclass_create,
|
|
44
|
+
gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
|
|
45
|
+
gobj_start, gobj_send_event, gobj_current_state,
|
|
46
|
+
set_log_callback,
|
|
47
|
+
} = await import("@yuneta/gobj-js");
|
|
48
|
+
const {register_c_yui_shell} = await import("./c_yui_shell.js");
|
|
49
|
+
const {register_c_yui_json} = await import("./c_yui_json.js");
|
|
50
|
+
const {register_c_yui_treedb_topic_with_form} = await import("./c_yui_treedb_topic_with_form.js");
|
|
51
|
+
|
|
52
|
+
const logged = [];
|
|
53
|
+
let yuno = null;
|
|
54
|
+
|
|
55
|
+
beforeAll(async () => {
|
|
56
|
+
/* The app's i18next, with no locale: a key answers itself. */
|
|
57
|
+
const i18next = (await import("i18next")).default;
|
|
58
|
+
await i18next.init({lng: "en", resources: {}});
|
|
59
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
60
|
+
set_log_callback((level, msg) => {
|
|
61
|
+
logged.push({level: String(level), msg: String(msg)});
|
|
62
|
+
});
|
|
63
|
+
gclass_create("C_TEST_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
64
|
+
register_c_yui_shell();
|
|
65
|
+
register_c_yui_json();
|
|
66
|
+
register_c_yui_treedb_topic_with_form();
|
|
67
|
+
yuno = gobj_create_yuno("form_json_yuno", "C_TEST_HOST", {});
|
|
68
|
+
gobj_start(yuno);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
logged.length = 0;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const DESC = {
|
|
76
|
+
id: "users",
|
|
77
|
+
pkey: "id",
|
|
78
|
+
cols: {
|
|
79
|
+
id: {id: "id", header: "Id", type: "string", flag: ["persistent", "required"]},
|
|
80
|
+
name: {id: "name", header: "Name", type: "string", flag: ["persistent", "writable"]}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function errors()
|
|
85
|
+
{
|
|
86
|
+
return logged.filter((l) => l.level === "error").map((l) => l.msg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("a viewer of the form asks to load a truncated part", () => {
|
|
90
|
+
|
|
91
|
+
test("the form answers that it cannot, and the stub stops loading", () => {
|
|
92
|
+
const app = gobj_create("f1_app", "C_TEST_HOST", {}, yuno);
|
|
93
|
+
const shell = gobj_create("f1_shell", "C_YUI_SHELL",
|
|
94
|
+
{config: {shell: {}}, default_route: "/home", subscriber: app}, app);
|
|
95
|
+
gobj_start(shell);
|
|
96
|
+
const form = gobj_create_service("f1_form", "C_YUI_TREEDB_TOPIC_WITH_FORM", {
|
|
97
|
+
treedb_name: "treedb_test",
|
|
98
|
+
topic_name: "users",
|
|
99
|
+
desc: DESC
|
|
100
|
+
}, shell);
|
|
101
|
+
gobj_start(form);
|
|
102
|
+
|
|
103
|
+
logged.length = 0;
|
|
104
|
+
gobj_send_event(form, "EV_SHOW_SCHEMA", {}, form);
|
|
105
|
+
const viewer = form.priv.schema_gobj;
|
|
106
|
+
expect(viewer).toBeTruthy();
|
|
107
|
+
expect(gobj_current_state(viewer)).toBe("ST_READY");
|
|
108
|
+
logged.length = 0;
|
|
109
|
+
|
|
110
|
+
/* What the stub's click sends: the viewer then PUBLISHES
|
|
111
|
+
* EV_EXPAND_PATH to its host. */
|
|
112
|
+
gobj_send_event(viewer, "EV_EXPAND_COLLAPSED", {path: "cols`name", size: 40}, viewer);
|
|
113
|
+
|
|
114
|
+
expect(viewer.priv.pending.has("cols`name")).toBe(false);
|
|
115
|
+
expect(viewer.priv.errors.get("cols`name")).toBe("this part cannot be loaded here");
|
|
116
|
+
/* The viewer logs what it could not load; nothing else is. */
|
|
117
|
+
expect(errors()).toEqual([
|
|
118
|
+
"C_YUI_JSON: subtree load failed at 'cols`name': this part cannot be loaded here"
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -188,6 +188,43 @@ describe("a refused __graphs__ write keeps the Save lit", () => {
|
|
|
188
188
|
expect(errors()).toEqual([]);
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
/* Before gobj-ui 7.25.19 only a Save that planned the topic paid
|
|
192
|
+
* what was owed. Save #1 and Save #2 both write `users`, #1 is
|
|
193
|
+
* refused and its answer lands after #2's echo: the echo made the
|
|
194
|
+
* topic saved, so no Save planned it again, and the button stayed
|
|
195
|
+
* lit over nothing to write until a reload. */
|
|
196
|
+
test("an echo of the topic pays what was owed: the Save goes dark", () => {
|
|
197
|
+
const g = build("edition");
|
|
198
|
+
g.tree.priv.descs = {};
|
|
199
|
+
g.tree.priv.__graphs__ = [];
|
|
200
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
201
|
+
expect(g.lit()).toBe(true);
|
|
202
|
+
|
|
203
|
+
gobj_send_event(g.tree, "EV_NODE_UPDATED", {
|
|
204
|
+
topic_name: "__graphs__",
|
|
205
|
+
node: {id: "users", topic: "users", active: true,
|
|
206
|
+
properties: {nodes: {u1: {x: 30, y: 40}}}}
|
|
207
|
+
}, g.tree);
|
|
208
|
+
expect(g.lit()).toBe(false);
|
|
209
|
+
|
|
210
|
+
gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
|
|
211
|
+
expect(published.filter((p) => p.event === "EV_UPDATE_NODE")).toEqual([]);
|
|
212
|
+
expect(errors()).toEqual([]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("an echo of ANOTHER topic leaves it owed", () => {
|
|
216
|
+
const g = build("edition");
|
|
217
|
+
g.tree.priv.descs = {};
|
|
218
|
+
g.tree.priv.__graphs__ = [];
|
|
219
|
+
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
220
|
+
gobj_send_event(g.tree, "EV_NODE_UPDATED", {
|
|
221
|
+
topic_name: "__graphs__",
|
|
222
|
+
node: {id: "roles", topic: "roles", active: true, properties: {nodes: {}}}
|
|
223
|
+
}, g.tree);
|
|
224
|
+
expect(g.lit()).toBe(true);
|
|
225
|
+
expect(errors()).toEqual([]);
|
|
226
|
+
});
|
|
227
|
+
|
|
191
228
|
test("a reload forgets it: the view holds what the backend holds", () => {
|
|
192
229
|
const g = build("edition");
|
|
193
230
|
gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED", {topic: "users"}, g.tree);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* node_nav_events.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* C_YUI_NODE hosts C_YUI_NAV as a pure CHILD, so it hears every
|
|
5
|
+
* event the nav publishes -- and it declared only EV_NAV_CLICKED.
|
|
6
|
+
* EV_NAV_ITEM_CLOSE (the ✕ of a closable item) and
|
|
7
|
+
* EV_DRAWER_CLOSE_REQUESTED (the backdrop of a drawer projection,
|
|
8
|
+
* a layout a node may declare) answered "Event NOT DEFINED in
|
|
9
|
+
* state" -- and the drawer stayed open, because the shell that
|
|
10
|
+
* closes drawers knows only its own navs. A zone projection also
|
|
11
|
+
* outlives the path (it is stopped only in mt_stop), so the clicks
|
|
12
|
+
* of a root that is off the path landed on ST_OFF, which declared
|
|
13
|
+
* none of the three.
|
|
14
|
+
*
|
|
15
|
+
* Driven through the real gclasses (C_YUI_SHELL, C_YUI_NODE,
|
|
16
|
+
* C_YUI_NAV) on a document double: each event is published by the
|
|
17
|
+
* real nav the node created.
|
|
18
|
+
*
|
|
19
|
+
* Copyright (c) 2026, ArtGins.
|
|
20
|
+
* All Rights Reserved.
|
|
21
|
+
***********************************************************************/
|
|
22
|
+
import {describe, test, expect, beforeAll, beforeEach} from "vitest";
|
|
23
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
24
|
+
|
|
25
|
+
install_dom_double();
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
SDATA_END, gclass_create,
|
|
29
|
+
gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_pure_child,
|
|
30
|
+
gobj_start, gobj_send_event, gobj_publish_event, gobj_read_attr,
|
|
31
|
+
gobj_current_state, gobj_change_state,
|
|
32
|
+
set_log_callback,
|
|
33
|
+
} = await import("@yuneta/gobj-js");
|
|
34
|
+
const {register_c_yui_shell} = await import("./c_yui_shell.js");
|
|
35
|
+
const {register_c_yui_nav} = await import("./c_yui_nav.js");
|
|
36
|
+
const {register_c_yui_node} = await import("./c_yui_node.js");
|
|
37
|
+
|
|
38
|
+
const logged = [];
|
|
39
|
+
const closed = []; /* the EV_NAV_ITEM_CLOSE the app heard */
|
|
40
|
+
let yuno = null;
|
|
41
|
+
|
|
42
|
+
beforeAll(() => {
|
|
43
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
44
|
+
set_log_callback((level, msg) => {
|
|
45
|
+
logged.push({level: String(level), msg: String(msg)});
|
|
46
|
+
});
|
|
47
|
+
gclass_create("C_TEST_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
48
|
+
/* An app that uses closable items: it declares what the shell
|
|
49
|
+
* re-publishes. */
|
|
50
|
+
gclass_create(
|
|
51
|
+
"C_TEST_APP",
|
|
52
|
+
[["EV_NAV_ITEM_CLOSE", 0]],
|
|
53
|
+
[["ST_IDLE", [
|
|
54
|
+
["EV_NAV_ITEM_CLOSE", (gobj, event, kw) => {
|
|
55
|
+
closed.push(kw);
|
|
56
|
+
return 0;
|
|
57
|
+
}, null]
|
|
58
|
+
]]],
|
|
59
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
60
|
+
);
|
|
61
|
+
register_c_yui_shell();
|
|
62
|
+
register_c_yui_nav();
|
|
63
|
+
register_c_yui_node();
|
|
64
|
+
yuno = gobj_create_yuno("node_nav_yuno", "C_TEST_HOST", {});
|
|
65
|
+
gobj_start(yuno);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
logged.length = 0;
|
|
70
|
+
closed.length = 0;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let seq = 0;
|
|
74
|
+
|
|
75
|
+
/* A tree under a shell, on the path, projecting its children with
|
|
76
|
+
* `layout`. Returns the tree and the nav it created. */
|
|
77
|
+
function build(layout)
|
|
78
|
+
{
|
|
79
|
+
const name = `n${++seq}`;
|
|
80
|
+
const app = gobj_create(`${name}_app`, "C_TEST_APP", {}, yuno);
|
|
81
|
+
gobj_start(app);
|
|
82
|
+
const shell = gobj_create(`${name}_shell`, "C_YUI_SHELL",
|
|
83
|
+
{config: {shell: {}}, default_route: "/home", subscriber: app}, app);
|
|
84
|
+
gobj_start(shell);
|
|
85
|
+
const tree = gobj_create_pure_child(`${name}_tree`, "C_YUI_NODE", {
|
|
86
|
+
node_id: "root",
|
|
87
|
+
base_route: "/tree",
|
|
88
|
+
projection: {index: {layout: layout}},
|
|
89
|
+
children: [{id: "a", label: "A"}, {id: "b", label: "B"}]
|
|
90
|
+
}, shell);
|
|
91
|
+
gobj_start(tree);
|
|
92
|
+
gobj_send_event(tree, "EV_ROUTE_CHANGED", {base: "/tree", subpath: ""}, shell);
|
|
93
|
+
expect(gobj_current_state(tree)).toBe("ST_SELF");
|
|
94
|
+
const nav = tree.priv.navs[0];
|
|
95
|
+
expect(nav).toBeTruthy();
|
|
96
|
+
expect(gobj_read_attr(nav, "layout")).toBe(layout);
|
|
97
|
+
logged.length = 0;
|
|
98
|
+
return {app, shell, tree, nav};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function errors()
|
|
102
|
+
{
|
|
103
|
+
return logged.filter((l) => l.level === "error").map((l) => l.msg);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("the events of a C_YUI_NAV reach its C_YUI_NODE", () => {
|
|
107
|
+
|
|
108
|
+
test("EV_NAV_ITEM_CLOSE is handed to the shell, and the app hears it", () => {
|
|
109
|
+
const {nav} = build("tabs");
|
|
110
|
+
gobj_publish_event(nav, "EV_NAV_ITEM_CLOSE", {
|
|
111
|
+
item_id: "a", route: "/tree/a", menu_id: "node./tree", zone: ""
|
|
112
|
+
});
|
|
113
|
+
expect(errors()).toEqual([]);
|
|
114
|
+
expect(closed).toEqual([{
|
|
115
|
+
item_id: "a", route: "/tree/a", menu_id: "node./tree", zone: ""
|
|
116
|
+
}]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("EV_DRAWER_CLOSE_REQUESTED closes the node's own drawer", () => {
|
|
120
|
+
const {nav} = build("drawer");
|
|
121
|
+
const $drawer = gobj_read_attr(nav, "$container");
|
|
122
|
+
$drawer.classList.add("is-active");
|
|
123
|
+
|
|
124
|
+
gobj_publish_event(nav, "EV_DRAWER_CLOSE_REQUESTED", {
|
|
125
|
+
menu_id: gobj_read_attr(nav, "menu_id")
|
|
126
|
+
});
|
|
127
|
+
expect(errors()).toEqual([]);
|
|
128
|
+
expect($drawer.classList.contains("is-active")).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("a drawer of another menu is left open", () => {
|
|
132
|
+
const {nav} = build("drawer");
|
|
133
|
+
const $drawer = gobj_read_attr(nav, "$container");
|
|
134
|
+
$drawer.classList.add("is-active");
|
|
135
|
+
gobj_publish_event(nav, "EV_DRAWER_CLOSE_REQUESTED", {menu_id: "other"});
|
|
136
|
+
expect(errors()).toEqual([]);
|
|
137
|
+
expect($drawer.classList.contains("is-active")).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("off the path, a nav that is still there is heard too", () => {
|
|
141
|
+
const {tree, nav} = build("tabs");
|
|
142
|
+
/* What a zone nav is: alive while its root is off the path. */
|
|
143
|
+
gobj_change_state(tree, "ST_OFF");
|
|
144
|
+
gobj_publish_event(nav, "EV_NAV_ITEM_CLOSE", {item_id: "b"});
|
|
145
|
+
gobj_publish_event(nav, "EV_DRAWER_CLOSE_REQUESTED", {menu_id: "x"});
|
|
146
|
+
gobj_publish_event(nav, "EV_NAV_CLICKED", {route: "/tree/a", item_id: "a"});
|
|
147
|
+
expect(errors()).toEqual([]);
|
|
148
|
+
expect(closed.map((c) => c.item_id)).toEqual(["b"]);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -481,6 +481,43 @@ describe("the drawing of a treedb that is not there", () => {
|
|
|
481
481
|
});
|
|
482
482
|
});
|
|
483
483
|
|
|
484
|
+
/*
|
|
485
|
+
* Since gobj-ui 7.25.17 the notice translated its detail, and three
|
|
486
|
+
* callers pass a NAME there: a topic called `nodes` that is gone was
|
|
487
|
+
* shown as "Nodos" in an app whose locale has that key, and followed
|
|
488
|
+
* every change of language.
|
|
489
|
+
*/
|
|
490
|
+
describe("the name in a notice is a name", () => {
|
|
491
|
+
|
|
492
|
+
test("a topic that is gone is named as it is, with no i18n key", async () => {
|
|
493
|
+
const i18next = (await import("i18next")).default;
|
|
494
|
+
await i18next.init({lng: "es", resources: {es: {translation: {nodes: "Nodos", db: "BD"}}}});
|
|
495
|
+
try {
|
|
496
|
+
const {editor} = build("nn1", "db/nodes");
|
|
497
|
+
expect($in(editor, ".SCHEMA_NOTICE_TEXT").getAttribute("data-i18n"))
|
|
498
|
+
.toBe("that topic is not here any more");
|
|
499
|
+
const $detail = $in(editor, ".SCHEMA_NOTICE_DETAIL");
|
|
500
|
+
expect($detail.textContent).toBe("nodes");
|
|
501
|
+
expect($detail.getAttribute("data-i18n")).toBe(null);
|
|
502
|
+
} finally {
|
|
503
|
+
await i18next.init({lng: "en", resources: {}});
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("a treedb that is gone is named as it is, with no i18n key", async () => {
|
|
508
|
+
const i18next = (await import("i18next")).default;
|
|
509
|
+
await i18next.init({lng: "es", resources: {es: {translation: {nodes: "Nodos"}}}});
|
|
510
|
+
try {
|
|
511
|
+
const {editor} = build("nn2", "nodes");
|
|
512
|
+
const $detail = $in(editor, ".SCHEMA_NOTICE_DETAIL");
|
|
513
|
+
expect($detail.textContent).toBe("nodes");
|
|
514
|
+
expect($detail.getAttribute("data-i18n")).toBe(null);
|
|
515
|
+
} finally {
|
|
516
|
+
await i18next.init({lng: "en", resources: {}});
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
484
521
|
describe("a write given up that was DONE", () => {
|
|
485
522
|
|
|
486
523
|
test("the host is told, and the reload keeps what this session wrote", () => {
|
|
@@ -43,6 +43,7 @@ const {
|
|
|
43
43
|
} = await import("@yuneta/gobj-js");
|
|
44
44
|
|
|
45
45
|
const logged = [];
|
|
46
|
+
const written = []; /* the EV_RECORD_WRITTEN the host heard */
|
|
46
47
|
const commands = []; /* what the fake transport was asked */
|
|
47
48
|
const told = []; /* what the fake engine was told */
|
|
48
49
|
let refuse_on_the_way = false;
|
|
@@ -65,6 +66,20 @@ beforeAll(async () => {
|
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
gclass_create("C_TEST_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
69
|
+
/* The host of the view: it hears what the view publishes. */
|
|
70
|
+
gclass_create(
|
|
71
|
+
"C_TEST_GRAPH_HOST",
|
|
72
|
+
[["EV_RECORD_WRITTEN", 0], ["EV_TOPIC_SELECTED", 0], ["EV_OPERATION_MODE_CHANGED", 0]],
|
|
73
|
+
[["ST_IDLE", [
|
|
74
|
+
["EV_RECORD_WRITTEN", (gobj, event, kw) => {
|
|
75
|
+
written.push(kw);
|
|
76
|
+
return 0;
|
|
77
|
+
}, null],
|
|
78
|
+
["EV_TOPIC_SELECTED", () => 0, null],
|
|
79
|
+
["EV_OPERATION_MODE_CHANGED", () => 0, null]
|
|
80
|
+
]]],
|
|
81
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
82
|
+
);
|
|
68
83
|
gclass_create(
|
|
69
84
|
"C_TEST_REMOTE",
|
|
70
85
|
[],
|
|
@@ -120,6 +135,7 @@ beforeAll(async () => {
|
|
|
120
135
|
|
|
121
136
|
beforeEach(() => {
|
|
122
137
|
logged.length = 0;
|
|
138
|
+
written.length = 0;
|
|
123
139
|
commands.length = 0;
|
|
124
140
|
told.length = 0;
|
|
125
141
|
refuse_on_the_way = false;
|
|
@@ -129,9 +145,11 @@ function build(name)
|
|
|
129
145
|
{
|
|
130
146
|
const remote = gobj_create_service(`${name}_remote`, "C_TEST_REMOTE", {}, yuno);
|
|
131
147
|
gobj_change_state(remote, "ST_SESSION");
|
|
148
|
+
const view_host = gobj_create(`${name}_view_host`, "C_TEST_GRAPH_HOST", {}, yuno);
|
|
132
149
|
const host = gobj_create_service(`${name}_graph`, "C_YUI_TREEDB_GRAPH", {
|
|
133
150
|
gobj_remote_yuno: remote,
|
|
134
|
-
treedb_name: "treedb_test"
|
|
151
|
+
treedb_name: "treedb_test",
|
|
152
|
+
subscriber: view_host
|
|
135
153
|
}, yuno);
|
|
136
154
|
const engine = host.priv.gobj_nodes_tree;
|
|
137
155
|
expect(engine).toBeTruthy();
|
|
@@ -154,13 +172,13 @@ function save_arrangement(engine, topic)
|
|
|
154
172
|
* mt_command, gui_agent's C_AGENT_TREEDB_LINK), never the request's
|
|
155
173
|
* own kw. Echoing the whole kw here once hid that the refusal read
|
|
156
174
|
* its topic from a `record` no real answer carries. */
|
|
157
|
-
function answer(host, remote, request, result, comment)
|
|
175
|
+
function answer(host, remote, request, result, comment, data)
|
|
158
176
|
{
|
|
159
177
|
const md_command = request.kw.__md_command__ || {};
|
|
160
178
|
gobj_send_event(host, "EV_MT_COMMAND_ANSWER", {
|
|
161
179
|
result: result,
|
|
162
180
|
comment: comment || "",
|
|
163
|
-
data: null,
|
|
181
|
+
data: (data === undefined) ? null : data,
|
|
164
182
|
__md_iev__: {command_stack: [{command: request.command, kw: md_command}]}
|
|
165
183
|
}, remote);
|
|
166
184
|
}
|
|
@@ -213,3 +231,53 @@ describe("a __graphs__ write that does not land is said to the engine", () => {
|
|
|
213
231
|
expect(refused()).toEqual([]);
|
|
214
232
|
});
|
|
215
233
|
});
|
|
234
|
+
|
|
235
|
+
/* What THIS view wrote, told to its host. The answer carries back only
|
|
236
|
+
* `__md_command__`: `treedb_name` and `record` read from that frame
|
|
237
|
+
* arrived empty with every real transport. */
|
|
238
|
+
describe("EV_RECORD_WRITTEN carries what it says", () => {
|
|
239
|
+
|
|
240
|
+
test("an update: the treedb, the topic and the record the store answered", () => {
|
|
241
|
+
const {remote, host, engine} = build("w1");
|
|
242
|
+
logged.length = 0; /* the fake engine's fixture, not the check */
|
|
243
|
+
gobj_send_event(host, "EV_UPDATE_NODE", {
|
|
244
|
+
topic_name: "yunos", record: {id: "1", x: 1}
|
|
245
|
+
}, engine);
|
|
246
|
+
const [write] = commands;
|
|
247
|
+
answer(host, remote, write, 0, "", {id: "1", x: 1, y: 2});
|
|
248
|
+
expect(written).toEqual([{
|
|
249
|
+
treedb_name: "treedb_test",
|
|
250
|
+
topic_name: "yunos",
|
|
251
|
+
record: {id: "1", x: 1, y: 2},
|
|
252
|
+
created: false,
|
|
253
|
+
command: "update-node"
|
|
254
|
+
}]);
|
|
255
|
+
expect(logged.filter((l) => l.level === "error")).toEqual([]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("a link: the child's topic, both refs, and the child the store answered", () => {
|
|
259
|
+
const {remote, host, engine} = build("w2");
|
|
260
|
+
gobj_send_event(host, "EV_LINK_NODES", {
|
|
261
|
+
parent_ref: "realms^r1^yunos", child_ref: "yunos^1"
|
|
262
|
+
}, engine);
|
|
263
|
+
const [link] = commands;
|
|
264
|
+
expect(link.command).toBe("link-nodes");
|
|
265
|
+
answer(host, remote, link, 0, "", {id: "1", realms: ["realms^r1^yunos"]});
|
|
266
|
+
expect(written).toEqual([{
|
|
267
|
+
treedb_name: "treedb_test",
|
|
268
|
+
topic_name: "yunos",
|
|
269
|
+
record: {id: "1", realms: ["realms^r1^yunos"]},
|
|
270
|
+
parent_ref: "realms^r1^yunos",
|
|
271
|
+
child_ref: "yunos^1",
|
|
272
|
+
created: false,
|
|
273
|
+
command: "link-nodes"
|
|
274
|
+
}]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("the arrangement (__graphs__) is not reported", () => {
|
|
278
|
+
const {remote, host, engine} = build("w3");
|
|
279
|
+
save_arrangement(engine, "yunos");
|
|
280
|
+
answer(host, remote, commands[0], 0, "", {id: "yunos"});
|
|
281
|
+
expect(written).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
});
|