@yuneta/gobj-ui 7.25.19 → 7.25.21

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.
@@ -1312,6 +1312,42 @@ function close_json_viewer(gobj)
1312
1312
  }
1313
1313
  }
1314
1314
 
1315
+ /************************************************************
1316
+ * A drill (or the whole document) that will get no answer from the
1317
+ * backend is answered HERE, or its stub shows "loading" for the life
1318
+ * of the viewer: C_YUI_JSON ignores every click on a pending path.
1319
+ * `answer` is {i18n: <key>} or {error: <text>}.
1320
+ ************************************************************/
1321
+ function answer_print_tranger_refused(gobj, path, answer)
1322
+ {
1323
+ let jv = gobj.priv.json_gobj;
1324
+ if(!jv || !is_gobj(jv) || gobj_is_destroying(jv)) {
1325
+ return; /* no viewer to answer: the refusal is already logged */
1326
+ }
1327
+ if(path) {
1328
+ gobj_send_event(jv, "EV_SUBTREE_ERROR",
1329
+ Object.assign({path: path}, answer), gobj);
1330
+ } else {
1331
+ yui_shell_show_error(yui_shell_of(gobj),
1332
+ answer.i18n || answer.error, {t: t});
1333
+ }
1334
+ }
1335
+
1336
+ /************************************************************
1337
+ * The text of a transport's refusal: gobj_command() answers a string,
1338
+ * a command response or a number.
1339
+ ************************************************************/
1340
+ function refusal_text(ret)
1341
+ {
1342
+ if(typeof ret === "string" && ret) {
1343
+ return ret;
1344
+ }
1345
+ if(ret && typeof ret === "object" && typeof ret.comment === "string" && ret.comment) {
1346
+ return ret.comment;
1347
+ }
1348
+ return "print-tranger failed";
1349
+ }
1350
+
1315
1351
  /************************************************************
1316
1352
  * Fetch the treedb's raw tranger (or one subtree when `path` is set) as
1317
1353
  * bounded, drillable JSON. Collapsed at 100 so a huge tranger stays a
@@ -1322,11 +1358,13 @@ function request_print_tranger(gobj, path)
1322
1358
  let priv = gobj.priv;
1323
1359
  if(!priv.gobj_remote_yuno) {
1324
1360
  log_error(`${gobj_short_name(gobj)}: No gobj_remote_yuno defined`);
1325
- let jv = priv.json_gobj;
1326
- if(path && jv && is_gobj(jv) && !gobj_is_destroying(jv)) {
1327
- gobj_send_event(jv, "EV_SUBTREE_ERROR",
1328
- {path: path, error: t("no session")}, gobj);
1329
- }
1361
+ answer_print_tranger_refused(gobj, path, {i18n: "no session"});
1362
+ return;
1363
+ }
1364
+ if(!is_connected(gobj)) {
1365
+ log_warning(`${gobj_short_name(gobj)}: print-tranger '${path || ""}' ` +
1366
+ `not asked: no session`);
1367
+ answer_print_tranger_refused(gobj, path, {i18n: "no session"});
1330
1368
  return;
1331
1369
  }
1332
1370
  let ret = gobj_command(priv.gobj_remote_yuno, "print-tranger",
@@ -1342,6 +1380,7 @@ function request_print_tranger(gobj, path)
1342
1380
  }, gobj);
1343
1381
  if(ret) {
1344
1382
  log_error(ret);
1383
+ answer_print_tranger_refused(gobj, path, {error: refusal_text(ret)});
1345
1384
  }
1346
1385
  }
1347
1386
 
@@ -1498,7 +1537,7 @@ function treedb_create_node(gobj, treedb_name, topic_name, record, options)
1498
1537
  /************************************************************
1499
1538
  * Command to remote service
1500
1539
  ************************************************************/
1501
- function treedb_update_node(gobj, treedb_name, topic_name, record, options)
1540
+ function treedb_update_node(gobj, treedb_name, topic_name, record, options, graphs_load)
1502
1541
  {
1503
1542
  let priv = gobj.priv;
1504
1543
 
@@ -1525,6 +1564,11 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
1525
1564
  * arranges, so that topic has to travel here. */
1526
1565
  if(topic_name === "__graphs__") {
1527
1566
  kw.__md_command__.graph_topic = graph_topic_of(record);
1567
+ /* And the engine's LOAD it was sent from: a refusal answered
1568
+ * after a reload is not about the arrangement shown now. */
1569
+ if(graphs_load !== undefined) {
1570
+ kw.__md_command__.graphs_load = graphs_load;
1571
+ }
1528
1572
  }
1529
1573
 
1530
1574
  let ret = gobj_command(priv.gobj_remote_yuno,
@@ -2162,7 +2206,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
2162
2206
  if(result < 0 && command === "update-node") {
2163
2207
  graphs_write_refused(gobj,
2164
2208
  kw_get_str(gobj, kw_command, "topic_name", "", 0),
2165
- kw_get_str(gobj, kw_command, "graph_topic", "", 0));
2209
+ kw_get_str(gobj, kw_command, "graph_topic", "", 0),
2210
+ kw_command ? kw_command.graphs_load : undefined);
2166
2211
  }
2167
2212
  if(result >= 0 &&
2168
2213
  kw_get_str(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") {
@@ -2533,9 +2578,10 @@ function ac_update_node(gobj, event, kw, src)
2533
2578
  let topic_name = kw.topic_name;
2534
2579
  let record = kw.record;
2535
2580
  let options = kw.options || {};
2581
+ let graphs_load = kw.graphs_load;
2536
2582
 
2537
2583
  if(refuse_if_readonly(gobj, event)) {
2538
- graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2584
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record), graphs_load);
2539
2585
  return -1;
2540
2586
  }
2541
2587
 
@@ -2545,7 +2591,7 @@ function ac_update_node(gobj, event, kw, src)
2545
2591
  if(topic_name === "__graphs__" && !is_connected(gobj)) {
2546
2592
  log_warning(`${gobj_short_name(gobj)}: no session, the arrangement ` +
2547
2593
  `was not sent`);
2548
- graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2594
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record), graphs_load);
2549
2595
  return -1;
2550
2596
  }
2551
2597
 
@@ -2554,10 +2600,11 @@ function ac_update_node(gobj, event, kw, src)
2554
2600
  treedb_name,
2555
2601
  topic_name,
2556
2602
  record,
2557
- options
2603
+ options,
2604
+ graphs_load
2558
2605
  );
2559
2606
  if(ret < 0) {
2560
- graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2607
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record), graphs_load);
2561
2608
  }
2562
2609
  return ret;
2563
2610
  }
@@ -2578,9 +2625,11 @@ function graph_topic_of(record)
2578
2625
  * (C_G6_NODES_TREE) took it for granted when it left -- nothing
2579
2626
  * answers it when it lands -- so it is told, and its next Save
2580
2627
  * writes the topic again. Other topics are the operator's data:
2581
- * the answer's error says what happened to them.
2628
+ * the answer's error says what happened to them. `graphs_load` is
2629
+ * the engine's load the write was sent from, handed back so it can
2630
+ * tell a refusal of an earlier load from one of the current.
2582
2631
  ************************************************************/
2583
- function graphs_write_refused(gobj, topic_name, topic)
2632
+ function graphs_write_refused(gobj, topic_name, topic, graphs_load)
2584
2633
  {
2585
2634
  let priv = gobj.priv;
2586
2635
 
@@ -2594,7 +2643,11 @@ function graphs_write_refused(gobj, topic_name, topic)
2594
2643
  if(!priv.gobj_nodes_tree) {
2595
2644
  return; /* no engine: nothing believes it was saved */
2596
2645
  }
2597
- gobj_send_event(priv.gobj_nodes_tree, "EV_GRAPHS_WRITE_REFUSED", {topic: topic}, gobj);
2646
+ let kw = {topic: topic};
2647
+ if(graphs_load !== undefined) {
2648
+ kw.graphs_load = graphs_load;
2649
+ }
2650
+ gobj_send_event(priv.gobj_nodes_tree, "EV_GRAPHS_WRITE_REFUSED", kw, gobj);
2598
2651
  }
2599
2652
 
2600
2653
  /********************************************
@@ -3711,8 +3711,9 @@ function ac_expand_path(gobj, event, kw, src)
3711
3711
  let path = (kw && kw.path) || "";
3712
3712
 
3713
3713
  gobj_send_event(src, "EV_SUBTREE_ERROR", {
3714
- path: path,
3715
- error: t("this part cannot be loaded here")
3714
+ path: path,
3715
+ i18n: "this part cannot be loaded here",
3716
+ by_design: true
3716
3717
  }, gobj);
3717
3718
  return 0;
3718
3719
  }
@@ -1943,6 +1943,42 @@ function request_schema_file(gobj)
1943
1943
  }
1944
1944
  }
1945
1945
 
1946
+ /************************************************************
1947
+ * A drill (or the whole document) that will get no answer from the
1948
+ * backend is answered HERE, or its stub shows "loading" for the life
1949
+ * of the viewer: C_YUI_JSON ignores every click on a pending path.
1950
+ * `answer` is {i18n: <key>} or {error: <text>}.
1951
+ ************************************************************/
1952
+ function answer_print_tranger_refused(gobj, path, answer)
1953
+ {
1954
+ let jv = gobj.priv.json_gobj;
1955
+ if(!jv || !is_gobj(jv) || gobj_is_destroying(jv)) {
1956
+ return; /* no viewer to answer: the refusal is already logged */
1957
+ }
1958
+ if(path) {
1959
+ gobj_send_event(jv, "EV_SUBTREE_ERROR",
1960
+ Object.assign({path: path}, answer), gobj);
1961
+ } else {
1962
+ yui_shell_show_error(yui_shell_of(gobj),
1963
+ answer.i18n || answer.error, {t: t});
1964
+ }
1965
+ }
1966
+
1967
+ /************************************************************
1968
+ * The text of a transport's refusal: gobj_command() answers a string,
1969
+ * a command response or a number.
1970
+ ************************************************************/
1971
+ function refusal_text(ret)
1972
+ {
1973
+ if(typeof ret === "string" && ret) {
1974
+ return ret;
1975
+ }
1976
+ if(ret && typeof ret === "object" && typeof ret.comment === "string" && ret.comment) {
1977
+ return ret.comment;
1978
+ }
1979
+ return "print-tranger failed";
1980
+ }
1981
+
1946
1982
  /************************************************************
1947
1983
  * The whole tranger, for the raw-json viewer.
1948
1984
  ************************************************************/
@@ -1952,11 +1988,13 @@ function request_print_tranger(gobj, path)
1952
1988
  let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
1953
1989
  if(!remote) {
1954
1990
  log_error(`${gobj_short_name(gobj)}: No gobj_remote_yuno defined`);
1955
- let jv = priv.json_gobj;
1956
- if(path && jv && is_gobj(jv) && !gobj_is_destroying(jv)) {
1957
- gobj_send_event(jv, "EV_SUBTREE_ERROR",
1958
- {path: path, error: t("no session")}, gobj);
1959
- }
1991
+ answer_print_tranger_refused(gobj, path, {i18n: "no session"});
1992
+ return;
1993
+ }
1994
+ if(!is_connected(gobj)) {
1995
+ log_warning(`${gobj_short_name(gobj)}: print-tranger '${path || ""}' ` +
1996
+ `not asked: no session`);
1997
+ answer_print_tranger_refused(gobj, path, {i18n: "no session"});
1960
1998
  return;
1961
1999
  }
1962
2000
  let ret = gobj_command(remote, "print-tranger",
@@ -1972,6 +2010,7 @@ function request_print_tranger(gobj, path)
1972
2010
  }, gobj);
1973
2011
  if(ret) {
1974
2012
  log_error(ret);
2013
+ answer_print_tranger_refused(gobj, path, {error: refusal_text(ret)});
1975
2014
  }
1976
2015
  }
1977
2016
 
@@ -43,10 +43,12 @@ const {
43
43
  SDATA_END, gclass_create,
44
44
  gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
45
45
  gobj_start, gobj_send_event, gobj_current_state,
46
+ gobj_create_pure_child, gobj_read_attr,
46
47
  set_log_callback,
47
48
  } = await import("@yuneta/gobj-js");
48
49
  const {register_c_yui_shell} = await import("./c_yui_shell.js");
49
50
  const {register_c_yui_json} = await import("./c_yui_json.js");
51
+ const {t} = await import("i18next");
50
52
  const {register_c_yui_treedb_topic_with_form} = await import("./c_yui_treedb_topic_with_form.js");
51
53
 
52
54
  const logged = [];
@@ -86,6 +88,11 @@ function errors()
86
88
  return logged.filter((l) => l.level === "error").map((l) => l.msg);
87
89
  }
88
90
 
91
+ function warnings()
92
+ {
93
+ return logged.filter((l) => l.level === "warning").map((l) => l.msg);
94
+ }
95
+
89
96
  describe("a viewer of the form asks to load a truncated part", () => {
90
97
 
91
98
  test("the form answers that it cannot, and the stub stops loading", () => {
@@ -112,10 +119,86 @@ describe("a viewer of the form asks to load a truncated part", () => {
112
119
  gobj_send_event(viewer, "EV_EXPAND_COLLAPSED", {path: "cols`name", size: 40}, viewer);
113
120
 
114
121
  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. */
122
+ /* The KEY travels, not its translation: the stub draws t(key)
123
+ * with data-i18n, so it changes language with the app. */
124
+ expect(viewer.priv.errors.get("cols`name")).toEqual(
125
+ {error: "this part cannot be loaded here", is_key: true});
126
+ /* A host saying it cannot, by design, is a WARNING: an ERROR
127
+ * on every click broke the clean-console check of a deploy and
128
+ * reached the backend through the remote log. */
129
+ expect(errors()).toEqual([]);
130
+ expect(warnings()).toEqual([
131
+ "C_YUI_JSON: subtree not loaded at 'cols`name': this part cannot be loaded here"
132
+ ]);
133
+
134
+ });
135
+ });
136
+
137
+ describe("the stub of a subtree that was not loaded", () => {
138
+
139
+ /* A host answering with a KEY that it cannot, by design. */
140
+ function make_viewer(name, answer)
141
+ {
142
+ gclass_create(`C_TEST_JSON_HOST_${name}`, [["EV_EXPAND_PATH", 0]], [
143
+ ["ST_IDLE", [["EV_EXPAND_PATH", function(gobj, event, kw, src) {
144
+ gobj_send_event(src, "EV_SUBTREE_ERROR",
145
+ Object.assign({path: kw.path}, answer), gobj);
146
+ return 0;
147
+ }, null]]]
148
+ ], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
149
+ const host = gobj_create(`${name}_host`, `C_TEST_JSON_HOST_${name}`, {}, yuno);
150
+ gobj_start(host);
151
+ const viewer = gobj_create_pure_child(`${name}_jv`, "C_YUI_JSON", {
152
+ json_data: {a: {__collapsed__: {size: 3, path: "a"}}}
153
+ }, host);
154
+ gobj_start(viewer);
155
+ return viewer;
156
+ }
157
+
158
+ function stub_error(viewer)
159
+ {
160
+ return gobj_read_attr(viewer, "$container").querySelector(".JSON_STUB_ERR");
161
+ }
162
+
163
+ test("a key is drawn translated, carries data-i18n, and follows the language", async () => {
164
+ const i18next = (await import("i18next")).default;
165
+ i18next.addResource("es", "translation",
166
+ "this part cannot be loaded here", "esta parte no se puede cargar aqui");
167
+
168
+ const viewer = make_viewer("s1",
169
+ {i18n: "this part cannot be loaded here", by_design: true});
170
+ logged.length = 0;
171
+ gobj_send_event(viewer, "EV_EXPAND_COLLAPSED", {path: "a", size: 3}, viewer);
172
+
173
+ let el = stub_error(viewer);
174
+ expect(el).toBeTruthy();
175
+ expect(el.getAttribute("data-i18n")).toBe("this part cannot be loaded here");
176
+ expect(el.textContent).toBe("this part cannot be loaded here");
177
+ expect(errors()).toEqual([]);
178
+ expect(warnings()).toEqual([
179
+ "C_YUI_JSON: subtree not loaded at 'a': this part cannot be loaded here"
180
+ ]);
181
+
182
+ await i18next.changeLanguage("es");
183
+ try {
184
+ gobj_send_event(viewer, "EV_LANGUAGE_CHANGED", {}, viewer);
185
+ expect(stub_error(viewer).textContent).toBe("esta parte no se puede cargar aqui");
186
+ expect(t("this part cannot be loaded here")).toBe("esta parte no se puede cargar aqui");
187
+ } finally {
188
+ await i18next.changeLanguage("en");
189
+ }
190
+ });
191
+
192
+ test("a failure in free text is drawn as it came, and is an ERROR", () => {
193
+ const viewer = make_viewer("s2", {error: "print-tranger failed"});
194
+ logged.length = 0;
195
+ gobj_send_event(viewer, "EV_EXPAND_COLLAPSED", {path: "a", size: 3}, viewer);
196
+
197
+ let el = stub_error(viewer);
198
+ expect(el.getAttribute("data-i18n")).toBe(null);
199
+ expect(el.textContent).toBe("print-tranger failed");
117
200
  expect(errors()).toEqual([
118
- "C_YUI_JSON: subtree load failed at 'cols`name': this part cannot be loaded here"
201
+ "C_YUI_JSON: subtree load failed at 'a': print-tranger failed"
119
202
  ]);
120
203
  });
121
204
  });
@@ -235,3 +235,80 @@ describe("a refused __graphs__ write keeps the Save lit", () => {
235
235
  expect(errors()).toEqual([]);
236
236
  });
237
237
  });
238
+
239
+ /* A refusal is about the LOAD its write was sent from. Before gobj-ui
240
+ * 7.25.20 it carried only the topic: Save, then Refresh, then the
241
+ * refusal landed and was applied to the FRESH load -- the Save lit
242
+ * over nothing changed, and pressing it wrote a record identical to
243
+ * what the backend holds. */
244
+ describe("a refusal of an earlier load is not applied to the current one", () => {
245
+
246
+ test("the write carries its load, and a refusal of it counts", () => {
247
+ const g = build("edition");
248
+ g.tree.priv._graph_properties.users.nodes.u1.x = 99;
249
+ gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
250
+ const [write] = published.filter((p) => p.event === "EV_UPDATE_NODE");
251
+ expect(typeof write.kw.graphs_load).toBe("number");
252
+
253
+ gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED",
254
+ {topic: "users", graphs_load: write.kw.graphs_load}, g.tree);
255
+ expect(g.lit()).toBe(true);
256
+ expect(g.tree.priv._saved_graph_properties.users).toBe(undefined);
257
+ expect(errors()).toEqual([]);
258
+ });
259
+
260
+ test("Save, reload, then the refusal: ignored, and said", () => {
261
+ const g = build("edition");
262
+ g.tree.priv._graph_properties.users.nodes.u1.x = 99;
263
+ gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
264
+ const [write] = published.filter((p) => p.event === "EV_UPDATE_NODE");
265
+
266
+ gobj_send_event(g.tree, "EV_CLEAR_DATA", {}, g.tree);
267
+ /* The reload brings what the backend holds. */
268
+ g.tree.priv._graph_properties = {users: {nodes: {u1: {x: 10, y: 20}}}};
269
+ g.tree.priv._saved_graph_properties = {users: {nodes: {u1: {x: 10, y: 20}}}};
270
+ gobj_send_event(g.tree, "EV_SET_OPERATION_MODE", {operation_mode: "edition"}, g.tree);
271
+ logged.length = 0;
272
+
273
+ gobj_send_event(g.tree, "EV_GRAPHS_WRITE_REFUSED",
274
+ {topic: "users", graphs_load: write.kw.graphs_load}, g.tree);
275
+ expect(g.lit()).toBe(false);
276
+ expect(g.tree.priv._saved_graph_properties.users).toEqual({nodes: {u1: {x: 10, y: 20}}});
277
+ expect(logged.map((l) => [l.level, l.msg.replace(/^\S+: /, "")])).toEqual([
278
+ ["warning", "a refused write of the arrangement of 'users' belongs to an earlier load: ignored"]
279
+ ]);
280
+ });
281
+ });
282
+
283
+ /* The FIRST record of a topic's arrangement arrives as a CREATE echo
284
+ * (from this browser or another). Before gobj-ui 7.25.20 it rebuilt
285
+ * every topic's saved copy from the live objects the view edits in
286
+ * place, so an unsaved change of ANOTHER topic counted as saved and
287
+ * the next Save did not write it. */
288
+ describe("a __graphs__ create echo moves only its own topic", () => {
289
+
290
+ test("an unsaved change of another topic stays unsaved", () => {
291
+ const g = build("edition");
292
+ g.tree.priv.descs = {};
293
+ g.tree.priv.__graphs__ = [
294
+ {id: "users", topic: "users", active: true,
295
+ properties: g.tree.priv._graph_properties.users}
296
+ ];
297
+ /* Edited in place, not saved. */
298
+ g.tree.priv._graph_properties.users.nodes.u1.x = 77;
299
+
300
+ gobj_send_event(g.tree, "EV_NODE_CREATED", {
301
+ topic_name: "__graphs__",
302
+ node: {id: "roles", topic: "roles", active: true,
303
+ properties: {nodes: {r1: {x: 1, y: 2}}}}
304
+ }, g.tree);
305
+
306
+ expect(g.tree.priv._saved_graph_properties.users).toEqual({nodes: {u1: {x: 10, y: 20}}});
307
+ expect(g.tree.priv._saved_graph_properties.roles).toEqual({nodes: {r1: {x: 1, y: 2}}});
308
+
309
+ gobj_send_event(g.tree, "EV_SAVE_GRAPH", {}, g.tree);
310
+ const writes = published.filter((p) => p.event === "EV_UPDATE_NODE");
311
+ expect(writes.map((w) => w.kw.record.id)).toEqual(["users"]);
312
+ expect(errors()).toEqual([]);
313
+ });
314
+ });
@@ -37,7 +37,7 @@ const {
37
37
  gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_service,
38
38
  gobj_start, gobj_send_event, gobj_read_attr,
39
39
  gobj_change_state, gobj_current_state,
40
- set_log_callback,
40
+ set_log_callback, log_error,
41
41
  } = await import("@yuneta/gobj-js");
42
42
  const {register_c_yui_schema_editor} = await import("./c_yui_schema_editor.js");
43
43
 
@@ -56,13 +56,33 @@ const RECORDS = {
56
56
  ],
57
57
  };
58
58
 
59
- /* A transport out of session refuses in the RETURN, as C_IEVENT_CLI and
60
- * the routing adapters do: a request that did not leave is not waited for. */
59
+ /* A transport out of session refuses in the RETURN, as the routing
60
+ * adapters do (gui_agent's C_AGENT_TREEDB_LINK): a request that did not
61
+ * leave is not waited for. `refuse_on_the_way` makes it refuse IN
62
+ * session too, as a transport that cannot send. */
63
+ let refuse_on_the_way = false;
64
+
61
65
  function remote_command_parser(gobj, command, kw, src)
62
66
  {
63
67
  if(gobj_current_state(gobj) !== "ST_SESSION") {
64
68
  return `${command}: not in session`;
65
69
  }
70
+ if(refuse_on_the_way) {
71
+ return `${command}: cannot send`;
72
+ }
73
+ commands.push({command, kw, src});
74
+ return null;
75
+ }
76
+
77
+ /* A DIRECT C_IEVENT_CLI, as its mt_command does it: out of session it
78
+ * logs "Not in session" and answers NULL -- the same answer as a request
79
+ * that left. The README allows `gobj_remote_yuno` to be one. */
80
+ function ievent_cli_command_parser(gobj, command, kw, src)
81
+ {
82
+ if(gobj_current_state(gobj) !== "ST_SESSION") {
83
+ log_error(`Not in session`);
84
+ return null;
85
+ }
66
86
  commands.push({command, kw, src});
67
87
  return null;
68
88
  }
@@ -93,6 +113,13 @@ beforeAll(() => {
93
113
  {mt_command_parser: remote_command_parser},
94
114
  0, [SDATA_END()], {}, 0, 0, 0, 0
95
115
  );
116
+ gclass_create(
117
+ "C_TEST_IEVENT_CLI",
118
+ [],
119
+ [["ST_DISCONNECTED", []], ["ST_SESSION", []]],
120
+ {mt_command_parser: ievent_cli_command_parser},
121
+ 0, [SDATA_END()], {}, 0, 0, 0, 0
122
+ );
96
123
  register_c_yui_schema_editor();
97
124
  yuno = gobj_create_yuno("transport_yuno", "C_TEST_HOST", {});
98
125
  gobj_start(yuno);
@@ -102,6 +129,7 @@ beforeEach(() => {
102
129
  logged.length = 0;
103
130
  commands.length = 0;
104
131
  shown.length = 0;
132
+ refuse_on_the_way = false;
105
133
  });
106
134
 
107
135
  function take(command)
@@ -148,9 +176,10 @@ function reconnect(editor, remote, host)
148
176
  gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: true}, host);
149
177
  }
150
178
 
151
- function build(name, loaded)
179
+ function build(name, loaded, remote_gclass)
152
180
  {
153
- const remote = gobj_create_service(`${name}_remote`, "C_TEST_REMOTE", {}, yuno);
181
+ const remote = gobj_create_service(`${name}_remote`,
182
+ remote_gclass || "C_TEST_REMOTE", {}, yuno);
154
183
  gobj_change_state(remote, "ST_SESSION");
155
184
  const host = gobj_create(`${name}_host`, "C_TEST_EDITOR_HOST", {}, yuno);
156
185
  const editor = gobj_create(`${name}_editor`, "C_YUI_SCHEMA_EDITOR", {
@@ -455,3 +484,58 @@ describe("a write answered after it was given up", () => {
455
484
  expect(errors()).toEqual([]);
456
485
  });
457
486
  });
487
+
488
+ /* Before gobj-ui 7.25.20 a write never asked whether the transport was
489
+ * in session. A direct C_IEVENT_CLI answers null out of session, which
490
+ * remote_command() took for "sent": the editor sat in ST_SAVING, busy
491
+ * and inert, until another drop. */
492
+ describe("a write with the transport out of session", () => {
493
+
494
+ test("a direct C_IEVENT_CLI: the write ends, said, and the next session reads again", () => {
495
+ const {editor, remote, host} = build("x1", true, "C_TEST_IEVENT_CLI");
496
+ /* Gone, and the host's edge has not arrived yet. */
497
+ gobj_change_state(remote, "ST_DISCONNECTED");
498
+
499
+ gobj_send_event(editor, "EV_SAVE_COLUMN", {
500
+ creating: true, topic: "users", values: {value: "name", type: "string"}
501
+ }, host);
502
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
503
+ expect(is_busy(editor)).toBe(false);
504
+ expect(take("update-node")).toEqual([]);
505
+ expect(shown).toEqual(["cannot reach the treedb"]);
506
+ expect(errors()).toEqual([]);
507
+ expect(warnings()).toEqual([
508
+ "C_YUI_SCHEMA_EDITOR^x1_editor: no session, 'update-node' of 'cols' was not sent"
509
+ ]);
510
+
511
+ gobj_send_event(editor, "EV_TRANSPORT_STATE", {connected: false}, host);
512
+ reconnect(editor, remote, host);
513
+ expect(answer_the_load(editor, remote)).toBe(3);
514
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
515
+ expect(errors()).toEqual([]);
516
+ });
517
+
518
+ /* The toast is handed the KEY (yui_shell_show_error translates it
519
+ * and keeps it for a change of language). This path passed
520
+ * t("cannot reach the treedb"), a text that cannot re-translate. */
521
+ test("a transport that refuses in session: the toast gets the key", async () => {
522
+ const i18next = (await import("i18next")).default;
523
+ if(!i18next.isInitialized) {
524
+ await i18next.init({lng: "en", resources: {}});
525
+ }
526
+ i18next.addResource(i18next.language || "en", "translation",
527
+ "cannot reach the treedb", "TRANSLATED: cannot reach the treedb");
528
+ try {
529
+ const {editor, remote, host} = build("x2", true);
530
+ refuse_on_the_way = true;
531
+ gobj_send_event(editor, "EV_SAVE_COLUMN", {
532
+ creating: true, topic: "users", values: {value: "name", type: "string"}
533
+ }, host);
534
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
535
+ expect(shown).toEqual(["cannot reach the treedb"]);
536
+ expect(errors()).toEqual(["update-node: cannot send"]);
537
+ } finally {
538
+ i18next.removeResourceBundle(i18next.language || "en", "translation");
539
+ }
540
+ });
541
+ });
@@ -156,15 +156,20 @@ function build(name)
156
156
  return {remote, host, engine};
157
157
  }
158
158
 
159
- /* What the engine publishes on a Save of one topic's arrangement. */
160
- function save_arrangement(engine, topic)
159
+ /* What the engine publishes on a Save of one topic's arrangement,
160
+ * from its load number `graphs_load`. */
161
+ function save_arrangement(engine, topic, graphs_load)
161
162
  {
162
- gobj_publish_event(engine, "EV_UPDATE_NODE", {
163
+ let kw = {
163
164
  treedb_name: "treedb_test",
164
165
  topic_name: "__graphs__",
165
166
  record: {id: topic, topic: topic, active: true, properties: {nodes: {}}},
166
167
  options: {list_dict: true, autolink: false, create: true}
167
- });
168
+ };
169
+ if(graphs_load !== undefined) {
170
+ kw.graphs_load = graphs_load;
171
+ }
172
+ gobj_publish_event(engine, "EV_UPDATE_NODE", kw);
168
173
  }
169
174
 
170
175
  /* The answer as the real transports build it: the command frame
@@ -188,6 +193,11 @@ function refused()
188
193
  return told.filter((t) => t.event === "EV_GRAPHS_WRITE_REFUSED").map((t) => t.kw.topic);
189
194
  }
190
195
 
196
+ function refused_kw()
197
+ {
198
+ return told.filter((t) => t.event === "EV_GRAPHS_WRITE_REFUSED").map((t) => t.kw);
199
+ }
200
+
191
201
  describe("a __graphs__ write that does not land is said to the engine", () => {
192
202
 
193
203
  test("refused by the backend", () => {
@@ -222,6 +232,30 @@ describe("a __graphs__ write that does not land is said to the engine", () => {
222
232
  expect(refused()).toEqual(["binaries"]);
223
233
  });
224
234
 
235
+ /* Before gobj-ui 7.25.20 the refusal carried only the topic, and
236
+ * the engine applied one answered after a reload to the fresh
237
+ * load. The engine's load number now travels in __md_command__
238
+ * and comes back with the refusal, on every path. */
239
+ test("the engine's load comes back with the refusal, on every path", () => {
240
+ const {remote, host, engine} = build("g6");
241
+ save_arrangement(engine, "yunos", 7);
242
+ expect(commands[0].kw.__md_command__.graphs_load).toBe(7);
243
+ answer(host, remote, commands[0], -1, "not authorized");
244
+
245
+ refuse_on_the_way = true;
246
+ save_arrangement(engine, "binaries", 7);
247
+ refuse_on_the_way = false;
248
+
249
+ gobj_change_state(remote, "ST_DISCONNECTED");
250
+ save_arrangement(engine, "realms", 8);
251
+
252
+ expect(refused_kw()).toEqual([
253
+ {topic: "yunos", graphs_load: 7},
254
+ {topic: "binaries", graphs_load: 7},
255
+ {topic: "realms", graphs_load: 8}
256
+ ]);
257
+ });
258
+
225
259
  test("a refused write of another topic says nothing to the engine", () => {
226
260
  const {remote, host, engine} = build("g5");
227
261
  gobj_send_event(host, "EV_UPDATE_NODE", {