@yuneta/gobj-ui 7.25.19 → 7.25.20

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.
@@ -494,6 +494,7 @@ let PRIVATE_DATA = {
494
494
  _graph_properties: {}, // topic_name → {nodes: {node_id: {x,y,size,...}}}
495
495
  _saved_graph_properties: {}, // the same, as the BACKEND has it: what a Save compares against
496
496
  _graphs_writes_owed: {}, // topic_name → true: a __graphs__ write the host said did not land
497
+ _graphs_load: 0, // the LOAD a __graphs__ write belongs to: EV_CLEAR_DATA starts another
497
498
  yet_showed: false,
498
499
  edit_mode: false,
499
500
  operation_mode: null,
@@ -3301,7 +3302,10 @@ function save_topic_graph_properties(gobj, topic_name)
3301
3302
  list_dict: true,
3302
3303
  autolink: false,
3303
3304
  create: true
3304
- }
3305
+ },
3306
+ /* Echoed back with a refusal (EV_GRAPHS_WRITE_REFUSED), so
3307
+ * one answered after a reload is told from one of THIS load. */
3308
+ graphs_load: priv._graphs_load
3305
3309
  };
3306
3310
  gobj_publish_event(gobj, "EV_UPDATE_NODE", kw_update);
3307
3311
  }
@@ -10372,6 +10376,9 @@ function ac_clear_data(gobj, event, kw, src)
10372
10376
  * reload brings what the backend holds, and that is what the view
10373
10377
  * will hold. */
10374
10378
  priv._graphs_writes_owed = {};
10379
+ /* And a write still on its way belongs to it: its refusal, if
10380
+ * one comes, is about an arrangement this view no longer holds. */
10381
+ priv._graphs_load++;
10375
10382
 
10376
10383
  /* The node counters belong to the data that is going away: a refresh
10377
10384
  * that kept them would decide the layout on the previous treedb. */
@@ -10537,12 +10544,19 @@ function ac_save_graph(gobj, event, kw, src)
10537
10544
  /************************************************************
10538
10545
  * The host says a write of `__graphs__` did not land: the
10539
10546
  * backend refused it, the transport refused it on the way out,
10540
- * or there was no session to send it on. kw: {topic}
10547
+ * or there was no session to send it on.
10548
+ * kw: {topic, graphs_load}
10541
10549
  *
10542
10550
  * save_topic_graph_properties() recorded it as saved before it
10543
10551
  * left. Forgotten now, and the Save is lit again: the next Save
10544
10552
  * writes the topic again. Before, the view believed the backend
10545
10553
  * held the arrangement, and the next Save had nothing to write.
10554
+ *
10555
+ * `graphs_load` is the load the write was sent from, echoed by the
10556
+ * host. A refusal of an EARLIER load is about an arrangement the
10557
+ * reload replaced with what the backend holds: applied, it lit the
10558
+ * Save over nothing changed (before gobj-ui 7.25.20). A host that
10559
+ * echoes no load is taken at its word, as before.
10546
10560
  ************************************************************/
10547
10561
  function ac_graphs_write_refused(gobj, event, kw, src)
10548
10562
  {
@@ -10553,6 +10567,11 @@ function ac_graphs_write_refused(gobj, event, kw, src)
10553
10567
  log_error(`${gobj_short_name(gobj)}: ${event} names no topic`);
10554
10568
  return -1;
10555
10569
  }
10570
+ if(kw.graphs_load !== undefined && kw.graphs_load !== priv._graphs_load) {
10571
+ log_warning(`${gobj_short_name(gobj)}: a refused write of the arrangement ` +
10572
+ `of '${topic_name}' belongs to an earlier load: ignored`);
10573
+ return 0;
10574
+ }
10556
10575
  log_warning(`${gobj_short_name(gobj)}: the arrangement of '${topic_name}' ` +
10557
10576
  `was not saved: the next Save writes it again`);
10558
10577
  forget_refused_graphs_write(priv._saved_graph_properties, topic_name);
@@ -10600,10 +10619,16 @@ function ac_node_created(gobj, event, kw, src)
10600
10619
  return 0;
10601
10620
  }
10602
10621
 
10603
- // Handle __graphs__ creation: update _graph_properties
10622
+ /* The first record of a topic's arrangement. Only THAT topic
10623
+ * moves, as for an update echo: build_graph_properties() took
10624
+ * every topic's saved copy from the live objects the view edits
10625
+ * in place, so an unsaved change of another topic counted as
10626
+ * saved and its Save was lost (before gobj-ui 7.25.20). */
10604
10627
  if(topic_name === '__graphs__') {
10605
10628
  priv.__graphs__.push(node);
10606
- build_graph_properties(gobj);
10629
+ apply_graphs_echo(
10630
+ priv._graph_properties, priv._saved_graph_properties, priv.__graphs__, node
10631
+ );
10607
10632
  settle_owed_graphs_write(gobj, node);
10608
10633
  return 0;
10609
10634
  }
package/src/c_yui_json.js CHANGED
@@ -64,6 +64,7 @@ import {
64
64
  gclass_create,
65
65
  gclass_find_by_name,
66
66
  log_error,
67
+ log_warning,
67
68
  gobj_read_pointer_attr,
68
69
  gobj_parent,
69
70
  gobj_subscribe_event,
@@ -898,8 +899,12 @@ function push_collapsed_row(ctx, value, segments, depth, key, rows)
898
899
  children.push(['span', {class: 'JSON_STUB_HINT is-size-7 ml-2',
899
900
  'data-i18n': is_pending ? 'loading' : 'click to load'},
900
901
  is_pending ? 'loading' : 'click to load']);
901
- if(err) {
902
- children.push(['span', {class: 'JSON_STUB_ERR has-text-danger is-size-7 ml-2'}, String(err)]);
902
+ if(err && err.is_key) {
903
+ children.push(['span', {class: 'JSON_STUB_ERR has-text-danger is-size-7 ml-2',
904
+ 'data-i18n': err.error}, t(err.error)]);
905
+ } else if(err) {
906
+ children.push(['span', {class: 'JSON_STUB_ERR has-text-danger is-size-7 ml-2'},
907
+ String(err.error)]);
903
908
  }
904
909
 
905
910
  let attrs = {
@@ -1117,17 +1122,35 @@ function ac_subtree_loaded(gobj, event, kw, src)
1117
1122
  }
1118
1123
 
1119
1124
  /************************************************************
1120
- * EV_SUBTREE_ERROR { path, error } — mark the failed branch
1125
+ * EV_SUBTREE_ERROR { path, error | i18n, by_design }
1126
+ * — mark the branch that was not loaded.
1127
+ *
1128
+ * `error` is text, drawn as it came (a backend's comment).
1129
+ * `i18n` is a KEY, drawn as t(key) with data-i18n so the stub
1130
+ * changes language with the app (a text translated by the host
1131
+ * stays in the language it was sent in). The name is the one a
1132
+ * locale check scans for (`i18n: "<key>"`), so the key of every
1133
+ * host stays visible to it.
1134
+ * `by_design`: the host answers that it CANNOT read a subtree
1135
+ * (a form holds records, a pasted pad has no backend) -- that is
1136
+ * not a failure, and it is logged as a warning. Anything else is
1137
+ * a load that failed, logged as an error.
1121
1138
  ************************************************************/
1122
1139
  function ac_subtree_error(gobj, event, kw, src)
1123
1140
  {
1124
1141
  let priv = gobj.priv;
1125
1142
 
1126
1143
  let path = kw.path || "";
1144
+ let is_key = !!kw.i18n;
1145
+ let error = kw.i18n || kw.error || "error";
1127
1146
  priv.pending.delete(path);
1128
- priv.errors.set(path, kw.error || "error");
1147
+ priv.errors.set(path, {error: error, is_key: is_key});
1129
1148
 
1130
- log_error(`${GCLASS_NAME}: subtree load failed at '${path}': ${kw.error || ""}`);
1149
+ if(kw.by_design) {
1150
+ log_warning(`${GCLASS_NAME}: subtree not loaded at '${path}': ${error}`);
1151
+ } else {
1152
+ log_error(`${GCLASS_NAME}: subtree load failed at '${path}': ${error}`);
1153
+ }
1131
1154
 
1132
1155
  render_view(gobj);
1133
1156
  return 0;
@@ -798,8 +798,9 @@ function ac_hide_diff(gobj, event, kw, src)
798
798
  function ac_expand_path(gobj, event, kw, src)
799
799
  {
800
800
  gobj_send_event(src, "EV_SUBTREE_ERROR", {
801
- path: (kw && kw.path) || "",
802
- error: t("collapsed in the source")
801
+ path: (kw && kw.path) || "",
802
+ i18n: "collapsed in the source",
803
+ by_design: true
803
804
  }, gobj);
804
805
  return 0;
805
806
  }
@@ -2820,6 +2820,19 @@ function run_next_write(gobj)
2820
2820
  let command = write_command(write.op);
2821
2821
  let options = write_options(write.op);
2822
2822
 
2823
+ /* Out of session a write is not sent, and ends here. A direct
2824
+ * C_IEVENT_CLI answers a command out of session with null -- the
2825
+ * answer of a request that LEFT -- so without this the editor
2826
+ * waited in ST_SAVING, inert, for an answer that never comes
2827
+ * (before gobj-ui 7.25.20). What the queue wrote before is not
2828
+ * known to the next session: it reads the model again. */
2829
+ if(!transport_in_session(gobj)) {
2830
+ log_warning(`${gobj_short_name(gobj)}: no session, '${command}' ` +
2831
+ `of '${write.topic_name}' was not sent`);
2832
+ priv.reload_on_open = true;
2833
+ return end_writes(gobj, "cannot reach the treedb");
2834
+ }
2835
+
2823
2836
  priv.write_tag++;
2824
2837
  if(remote_command(gobj, command, {
2825
2838
  topic_name: write.topic_name,
@@ -2827,7 +2840,7 @@ function run_next_write(gobj)
2827
2840
  options: options,
2828
2841
  record_id: write.record.id || ""
2829
2842
  }, {write: priv.write_tag}) < 0) {
2830
- return end_writes(gobj, t("cannot reach the treedb"));
2843
+ return end_writes(gobj, "cannot reach the treedb"); /* a key: the toast translates it */
2831
2844
  }
2832
2845
  return 0;
2833
2846
  }
@@ -1325,7 +1325,7 @@ function request_print_tranger(gobj, path)
1325
1325
  let jv = priv.json_gobj;
1326
1326
  if(path && jv && is_gobj(jv) && !gobj_is_destroying(jv)) {
1327
1327
  gobj_send_event(jv, "EV_SUBTREE_ERROR",
1328
- {path: path, error: t("no session")}, gobj);
1328
+ {path: path, i18n: "no session"}, gobj);
1329
1329
  }
1330
1330
  return;
1331
1331
  }
@@ -1498,7 +1498,7 @@ function treedb_create_node(gobj, treedb_name, topic_name, record, options)
1498
1498
  /************************************************************
1499
1499
  * Command to remote service
1500
1500
  ************************************************************/
1501
- function treedb_update_node(gobj, treedb_name, topic_name, record, options)
1501
+ function treedb_update_node(gobj, treedb_name, topic_name, record, options, graphs_load)
1502
1502
  {
1503
1503
  let priv = gobj.priv;
1504
1504
 
@@ -1525,6 +1525,11 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
1525
1525
  * arranges, so that topic has to travel here. */
1526
1526
  if(topic_name === "__graphs__") {
1527
1527
  kw.__md_command__.graph_topic = graph_topic_of(record);
1528
+ /* And the engine's LOAD it was sent from: a refusal answered
1529
+ * after a reload is not about the arrangement shown now. */
1530
+ if(graphs_load !== undefined) {
1531
+ kw.__md_command__.graphs_load = graphs_load;
1532
+ }
1528
1533
  }
1529
1534
 
1530
1535
  let ret = gobj_command(priv.gobj_remote_yuno,
@@ -2162,7 +2167,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
2162
2167
  if(result < 0 && command === "update-node") {
2163
2168
  graphs_write_refused(gobj,
2164
2169
  kw_get_str(gobj, kw_command, "topic_name", "", 0),
2165
- kw_get_str(gobj, kw_command, "graph_topic", "", 0));
2170
+ kw_get_str(gobj, kw_command, "graph_topic", "", 0),
2171
+ kw_command ? kw_command.graphs_load : undefined);
2166
2172
  }
2167
2173
  if(result >= 0 &&
2168
2174
  kw_get_str(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") {
@@ -2533,9 +2539,10 @@ function ac_update_node(gobj, event, kw, src)
2533
2539
  let topic_name = kw.topic_name;
2534
2540
  let record = kw.record;
2535
2541
  let options = kw.options || {};
2542
+ let graphs_load = kw.graphs_load;
2536
2543
 
2537
2544
  if(refuse_if_readonly(gobj, event)) {
2538
- graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2545
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record), graphs_load);
2539
2546
  return -1;
2540
2547
  }
2541
2548
 
@@ -2545,7 +2552,7 @@ function ac_update_node(gobj, event, kw, src)
2545
2552
  if(topic_name === "__graphs__" && !is_connected(gobj)) {
2546
2553
  log_warning(`${gobj_short_name(gobj)}: no session, the arrangement ` +
2547
2554
  `was not sent`);
2548
- graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2555
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record), graphs_load);
2549
2556
  return -1;
2550
2557
  }
2551
2558
 
@@ -2554,10 +2561,11 @@ function ac_update_node(gobj, event, kw, src)
2554
2561
  treedb_name,
2555
2562
  topic_name,
2556
2563
  record,
2557
- options
2564
+ options,
2565
+ graphs_load
2558
2566
  );
2559
2567
  if(ret < 0) {
2560
- graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2568
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record), graphs_load);
2561
2569
  }
2562
2570
  return ret;
2563
2571
  }
@@ -2578,9 +2586,11 @@ function graph_topic_of(record)
2578
2586
  * (C_G6_NODES_TREE) took it for granted when it left -- nothing
2579
2587
  * answers it when it lands -- so it is told, and its next Save
2580
2588
  * writes the topic again. Other topics are the operator's data:
2581
- * the answer's error says what happened to them.
2589
+ * the answer's error says what happened to them. `graphs_load` is
2590
+ * the engine's load the write was sent from, handed back so it can
2591
+ * tell a refusal of an earlier load from one of the current.
2582
2592
  ************************************************************/
2583
- function graphs_write_refused(gobj, topic_name, topic)
2593
+ function graphs_write_refused(gobj, topic_name, topic, graphs_load)
2584
2594
  {
2585
2595
  let priv = gobj.priv;
2586
2596
 
@@ -2594,7 +2604,11 @@ function graphs_write_refused(gobj, topic_name, topic)
2594
2604
  if(!priv.gobj_nodes_tree) {
2595
2605
  return; /* no engine: nothing believes it was saved */
2596
2606
  }
2597
- gobj_send_event(priv.gobj_nodes_tree, "EV_GRAPHS_WRITE_REFUSED", {topic: topic}, gobj);
2607
+ let kw = {topic: topic};
2608
+ if(graphs_load !== undefined) {
2609
+ kw.graphs_load = graphs_load;
2610
+ }
2611
+ gobj_send_event(priv.gobj_nodes_tree, "EV_GRAPHS_WRITE_REFUSED", kw, gobj);
2598
2612
  }
2599
2613
 
2600
2614
  /********************************************
@@ -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
  }
@@ -1955,7 +1955,7 @@ function request_print_tranger(gobj, path)
1955
1955
  let jv = priv.json_gobj;
1956
1956
  if(path && jv && is_gobj(jv) && !gobj_is_destroying(jv)) {
1957
1957
  gobj_send_event(jv, "EV_SUBTREE_ERROR",
1958
- {path: path, error: t("no session")}, gobj);
1958
+ {path: path, i18n: "no session"}, gobj);
1959
1959
  }
1960
1960
  return;
1961
1961
  }
@@ -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
+ });