@yuneta/gobj-ui 7.25.16 → 7.25.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.25.16",
3
+ "version": "7.25.18",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@rollup/plugin-terser": "^1.0.0",
32
32
  "@vitest/coverage-v8": "^4.1.10",
33
- "@yuneta/gobj-js": "^7.25.2",
33
+ "@yuneta/gobj-js": "^7.25.3",
34
34
  "vite": "^8.2.1",
35
35
  "vitest": "^4.1.10"
36
36
  },
@@ -403,7 +403,11 @@ function el_attrs(attrs)
403
403
  }
404
404
 
405
405
  /***************************************************************
406
- * The body stops answering while a write is in flight.
406
+ * The body stops answering while a write is in flight. For the
407
+ * keyboard too: `pointer-events` stops the mouse alone, and the
408
+ * shell's focus trap puts the focus back on the row control that
409
+ * had it, where Enter or Delete sent an action ST_SAVING does
410
+ * not declare. `inert` takes the whole body out of reach.
407
411
  ***************************************************************/
408
412
  function set_busy(gobj, busy)
409
413
  {
@@ -411,6 +415,7 @@ function set_busy(gobj, busy)
411
415
 
412
416
  if($body) {
413
417
  $body.classList.toggle("SCHEMA_BUSY", !!busy);
418
+ $body.toggleAttribute("inert", !!busy);
414
419
  }
415
420
  }
416
421
 
@@ -707,7 +712,7 @@ function request_model(gobj, keep_written)
707
712
  priv.records_before = null;
708
713
  priv.load_round++;
709
714
  priv.reload_on_open = true;
710
- priv.load_error = t("cannot reach the treedb");
715
+ priv.load_error = "cannot reach the treedb"; /* a key: the notice translates it */
711
716
  if(priv.model) {
712
717
  yui_shell_show_error(yui_shell_of(gobj), "cannot reach the treedb", {t: t});
713
718
  }
@@ -993,9 +998,14 @@ function show_notice(gobj, key, detail)
993
998
  $notice.appendChild(createElement2(
994
999
  ["span", {class: "SCHEMA_NOTICE_TEXT", i18n: key}, t(key)]
995
1000
  ));
996
- if(detail) {
1001
+ /* The detail is a KEY too (ours, or the backend's words, which
1002
+ * translate to themselves when no locale has them), so a change
1003
+ * of language changes it with the rest. One that repeats the
1004
+ * title says nothing. */
1005
+ if(detail && detail !== key) {
997
1006
  $notice.appendChild(createElement2(
998
- ["span", {class: "SCHEMA_NOTICE_DETAIL yui-text-quiet ml-2"}, `${detail}`]
1007
+ ["span", {class: "SCHEMA_NOTICE_DETAIL yui-text-quiet ml-2", i18n: `${detail}`},
1008
+ t(`${detail}`)]
999
1009
  ));
1000
1010
  }
1001
1011
  }
@@ -3350,7 +3360,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
3350
3360
  }
3351
3361
  priv.pending--;
3352
3362
  if(result < 0) {
3353
- priv.load_error = comment || t("cannot load the schemas");
3363
+ priv.load_error = comment || "cannot load the schemas"; /* a key */
3354
3364
  } else if(priv.records && Array.isArray(priv.records[topic_name])) {
3355
3365
  priv.records[topic_name] = Array.isArray(data) ? data : [];
3356
3366
  }
@@ -3427,10 +3437,11 @@ function ac_mt_command_answer(gobj, event, kw, src)
3427
3437
  return end_writes(gobj, comment || t("the treedb refused the write"));
3428
3438
  }
3429
3439
  priv.save_wrote = true;
3440
+ let record = null;
3430
3441
  if(command === "delete-node") {
3431
3442
  forget_record(gobj, topic_name, kw_get_str(gobj, md, "record_id", "", 0));
3432
3443
  } else {
3433
- let record = is_object(data) ? data
3444
+ record = is_object(data) ? data
3434
3445
  : (Array.isArray(data) && is_object(data[0]) ? data[0] : null);
3435
3446
  if(record) {
3436
3447
  patch_record(gobj, topic_name, record);
@@ -3453,7 +3464,7 @@ function ac_mt_command_answer(gobj, event, kw, src)
3453
3464
  }
3454
3465
  if(topic_name === T_TOPICS || topic_name === T_COLS) {
3455
3466
  let write = priv.save_queue[0];
3456
- mark_written(gobj, topic_name, write ? write.record : null);
3467
+ mark_written(gobj, topic_name, null, write ? write.record : null);
3457
3468
  }
3458
3469
  priv.reload_after_write = false;
3459
3470
  priv.save_queue = [];
@@ -3471,11 +3482,12 @@ function ac_mt_command_answer(gobj, event, kw, src)
3471
3482
  }
3472
3483
  }
3473
3484
  if(topic_name === T_TOPICS || topic_name === T_COLS) {
3474
- /* Which record it was is in the QUEUE, not in the answer: an
3475
- * adapter echoes only what it was asked to echo, and the
3476
- * record is not small enough to be worth echoing. */
3485
+ /* Which record it was: the one the store answered, else the
3486
+ * one in the QUEUE -- an adapter echoes only what it was
3487
+ * asked to echo, and the record is not small enough to be
3488
+ * worth echoing. */
3477
3489
  let write = priv.save_queue[0];
3478
- mark_written(gobj, topic_name, write ? write.record : null);
3490
+ mark_written(gobj, topic_name, record, write ? write.record : null);
3479
3491
  }
3480
3492
  priv.save_queue.shift();
3481
3493
  return run_next_write(gobj);
@@ -3544,35 +3556,46 @@ function late_write_done(gobj, command, topic_name, data)
3544
3556
  function mark_written_record(gobj, topic_name, record)
3545
3557
  {
3546
3558
  let priv = gobj.priv;
3559
+ let marked = 0;
3547
3560
 
3548
3561
  if(!record) {
3549
- return;
3562
+ return 0;
3550
3563
  }
3551
3564
  if(topic_name === T_TOPICS && typeof record.id === "string" && record.id) {
3552
3565
  priv.written[record.id] = true;
3553
- return;
3566
+ return 1;
3554
3567
  }
3555
3568
  if(topic_name !== T_COLS) {
3556
- return;
3569
+ return 0;
3557
3570
  }
3558
3571
  for(let ref of parse_fkey_ref(record.topics)) {
3559
3572
  priv.written[ref.id] = true;
3573
+ marked++;
3560
3574
  }
3575
+ return marked;
3561
3576
  }
3562
3577
 
3563
3578
  /***************************************************************
3564
3579
  * Remember which topic this write belongs to, which is what
3565
3580
  * "its version still has to move" is asked about.
3581
+ *
3582
+ * Asked of the RECORD first: the one the store answered (a new
3583
+ * topic has its id only there), then the one queued (a column
3584
+ * names its topic in its fkey). The screen is the last resort,
3585
+ * and a poor one: an import writes topics the operator is not
3586
+ * on, and from the topics screen there is no topic on it at all.
3566
3587
  ***************************************************************/
3567
- function mark_written(gobj, topic_name, record)
3588
+ function mark_written(gobj, topic_name, answered, queued)
3568
3589
  {
3569
3590
  let priv = gobj.priv;
3570
- let topic = current_topic(gobj);
3571
3591
 
3572
- if(topic_name === T_TOPICS && record && record.id) {
3573
- priv.written[record.id] = true;
3592
+ if(mark_written_record(gobj, topic_name, answered) > 0) {
3574
3593
  return;
3575
3594
  }
3595
+ if(mark_written_record(gobj, topic_name, queued) > 0) {
3596
+ return;
3597
+ }
3598
+ let topic = current_topic(gobj);
3576
3599
  if(topic) {
3577
3600
  priv.written[topic.id] = true;
3578
3601
  }
@@ -1520,6 +1520,12 @@ function treedb_update_node(gobj, treedb_name, topic_name, record, options)
1520
1520
  kw.__md_command__ = { // Data to be returned
1521
1521
  topic_name: topic_name,
1522
1522
  };
1523
+ /* The answer carries this echo and nothing else of the request: a
1524
+ * refused `__graphs__` write is told to the engine by the topic it
1525
+ * arranges, so that topic has to travel here. */
1526
+ if(topic_name === "__graphs__") {
1527
+ kw.__md_command__.graph_topic = graph_topic_of(record);
1528
+ }
1523
1529
 
1524
1530
  let ret = gobj_command(priv.gobj_remote_yuno,
1525
1531
  command,
@@ -1569,6 +1575,38 @@ function treedb_delete_node(gobj, treedb_name, topic_name, record, options)
1569
1575
  }
1570
1576
  }
1571
1577
 
1578
+ /************************************************************
1579
+ * What a link or an unlink asks back with its answer: the
1580
+ * topic of the CHILD (the node a link saves) and both refs.
1581
+ * `child_ref` is "topic^id", `parent_ref` "topic^id^hook".
1582
+ ************************************************************/
1583
+ function link_echo(parent_ref, child_ref)
1584
+ {
1585
+ let child_topic = (typeof child_ref === "string") ? child_ref.split("^")[0] : "";
1586
+ return {
1587
+ topic_name: child_topic,
1588
+ parent_ref: parent_ref,
1589
+ child_ref: child_ref
1590
+ };
1591
+ }
1592
+
1593
+ /************************************************************
1594
+ * The record a write answered: the node the store wrote (an
1595
+ * update or a create), deleted, or the child of a link. The
1596
+ * answer's frame carries back only `__md_command__`, never the
1597
+ * request's own record.
1598
+ ************************************************************/
1599
+ function answered_record(data)
1600
+ {
1601
+ if(data && typeof data === "object" && !Array.isArray(data)) {
1602
+ return data;
1603
+ }
1604
+ if(Array.isArray(data) && data[0] && typeof data[0] === "object") {
1605
+ return data[0];
1606
+ }
1607
+ return {};
1608
+ }
1609
+
1572
1610
  /************************************************************
1573
1611
  * Command to remote service
1574
1612
  ************************************************************/
@@ -1590,6 +1628,7 @@ function treedb_link_nodes(gobj, treedb_name, parent_ref, child_ref, options)
1590
1628
  child_ref: child_ref,
1591
1629
  options: options || {}
1592
1630
  };
1631
+ kw.__md_command__ = link_echo(parent_ref, child_ref);
1593
1632
 
1594
1633
  let ret = gobj_command(priv.gobj_remote_yuno,
1595
1634
  command,
@@ -1622,6 +1661,7 @@ function treedb_unlink_nodes(gobj, treedb_name, parent_ref, child_ref, options)
1622
1661
  child_ref: child_ref,
1623
1662
  options: options || {}
1624
1663
  };
1664
+ kw.__md_command__ = link_echo(parent_ref, child_ref);
1625
1665
 
1626
1666
  let ret = gobj_command(priv.gobj_remote_yuno,
1627
1667
  command,
@@ -2105,8 +2145,12 @@ function ac_mt_command_answer(gobj, event, kw, src)
2105
2145
  * answering its own writes in a loop.
2106
2146
  *
2107
2147
  * Same event and same kw as C_YUI_TREEDB_TOPICS, plus `command`:
2108
- * in a graph a LINK is as much a write as a record is, and the
2109
- * three of them carry different halves of the kw.
2148
+ * in a graph a LINK is as much a write as a record is, and a
2149
+ * link adds its two refs. Read from what THIS view knows and
2150
+ * from the answer, never from the request: the frame carries
2151
+ * back only `__md_command__`. `treedb_name` is the view's,
2152
+ * `record` is the node the store answered (the child, for a
2153
+ * link).
2110
2154
  *
2111
2155
  * `__graphs__` is EXCLUDED, and that exclusion is the point of
2112
2156
  * reading this comment: the view writes that topic ITSELF, one
@@ -2118,17 +2162,22 @@ function ac_mt_command_answer(gobj, event, kw, src)
2118
2162
  if(result < 0 && command === "update-node") {
2119
2163
  graphs_write_refused(gobj,
2120
2164
  kw_get_str(gobj, kw_command, "topic_name", "", 0),
2121
- kw_get_dict(gobj, kw_command, "record", {}, 0));
2165
+ kw_get_str(gobj, kw_command, "graph_topic", "", 0));
2122
2166
  }
2123
2167
  if(result >= 0 &&
2124
2168
  kw_get_str(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") {
2125
- gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
2126
- treedb_name: kw_get_str(gobj, kw_command, "treedb_name", "", 0),
2169
+ let written = {
2170
+ treedb_name: priv.treedb_name,
2127
2171
  topic_name: kw_get_str(gobj, kw_command, "topic_name", "", 0),
2128
- record: kw_get_dict(gobj, kw_command, "record", {}, 0),
2172
+ record: answered_record(data),
2129
2173
  created: (command === "create-node"),
2130
2174
  command: command
2131
- });
2175
+ };
2176
+ if(command === "link-nodes" || command === "unlink-nodes") {
2177
+ written.parent_ref = kw_get_str(gobj, kw_command, "parent_ref", "", 0);
2178
+ written.child_ref = kw_get_str(gobj, kw_command, "child_ref", "", 0);
2179
+ }
2180
+ gobj_publish_event(gobj, "EV_RECORD_WRITTEN", written);
2132
2181
  }
2133
2182
  break;
2134
2183
 
@@ -2486,7 +2535,7 @@ function ac_update_node(gobj, event, kw, src)
2486
2535
  let options = kw.options || {};
2487
2536
 
2488
2537
  if(refuse_if_readonly(gobj, event)) {
2489
- graphs_write_refused(gobj, topic_name, record);
2538
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2490
2539
  return -1;
2491
2540
  }
2492
2541
 
@@ -2496,7 +2545,7 @@ function ac_update_node(gobj, event, kw, src)
2496
2545
  if(topic_name === "__graphs__" && !is_connected(gobj)) {
2497
2546
  log_warning(`${gobj_short_name(gobj)}: no session, the arrangement ` +
2498
2547
  `was not sent`);
2499
- graphs_write_refused(gobj, topic_name, record);
2548
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2500
2549
  return -1;
2501
2550
  }
2502
2551
 
@@ -2508,11 +2557,22 @@ function ac_update_node(gobj, event, kw, src)
2508
2557
  options
2509
2558
  );
2510
2559
  if(ret < 0) {
2511
- graphs_write_refused(gobj, topic_name, record);
2560
+ graphs_write_refused(gobj, topic_name, graph_topic_of(record));
2512
2561
  }
2513
2562
  return ret;
2514
2563
  }
2515
2564
 
2565
+ /************************************************************
2566
+ * The topic a `__graphs__` record arranges.
2567
+ ************************************************************/
2568
+ function graph_topic_of(record)
2569
+ {
2570
+ if(!record || typeof record !== "object") {
2571
+ return "";
2572
+ }
2573
+ return record.topic || record.id || "";
2574
+ }
2575
+
2516
2576
  /************************************************************
2517
2577
  * A write of `__graphs__` did not land. The engine
2518
2578
  * (C_G6_NODES_TREE) took it for granted when it left -- nothing
@@ -2520,14 +2580,13 @@ function ac_update_node(gobj, event, kw, src)
2520
2580
  * writes the topic again. Other topics are the operator's data:
2521
2581
  * the answer's error says what happened to them.
2522
2582
  ************************************************************/
2523
- function graphs_write_refused(gobj, topic_name, record)
2583
+ function graphs_write_refused(gobj, topic_name, topic)
2524
2584
  {
2525
2585
  let priv = gobj.priv;
2526
2586
 
2527
2587
  if(topic_name !== "__graphs__") {
2528
2588
  return;
2529
2589
  }
2530
- let topic = (record && typeof record === "object") ? (record.topic || record.id) : "";
2531
2590
  if(!topic) {
2532
2591
  log_error(`${gobj_short_name(gobj)}: a refused __graphs__ write names no topic`);
2533
2592
  return;
@@ -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
  ************************************************************/
@@ -2210,10 +2227,14 @@ function ac_mt_command_answer(gobj, event, kw, src)
2210
2227
  * and cannot use the node events without answering its own writes
2211
2228
  * in a loop.
2212
2229
  */
2230
+ /* From what THIS view knows and from the answer, never from
2231
+ * the request: the frame carries back only `__md_command__`,
2232
+ * so `treedb_name` and `record` read there arrived empty.
2233
+ * `record` is the node the store answered, as it was written. */
2213
2234
  gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
2214
- treedb_name: kw_get_str(gobj, kw_command, "treedb_name", "", 0),
2235
+ treedb_name: gobj_read_str_attr(gobj, "treedb_name"),
2215
2236
  topic_name: kw_get_str(gobj, kw_command, "topic_name", "", 0),
2216
- record: kw_get_dict(gobj, kw_command, "record", {}, 0),
2237
+ record: answered_record(data),
2217
2238
  created: (command === "create-node"),
2218
2239
  command: command
2219
2240
  });
@@ -1501,3 +1501,164 @@ describe("EV_REFRESH while a write is in flight", () => {
1501
1501
  expect(not_defined()).toEqual([]);
1502
1502
  });
1503
1503
  });
1504
+
1505
+ describe("the body while a write is in flight", () => {
1506
+
1507
+ /* pointer-events stops the mouse and nothing else: the shell's
1508
+ * focus trap puts the focus back on the row control that had it,
1509
+ * and Delete or Enter there sent an action ST_SAVING does not
1510
+ * declare. `inert` takes the body out of the keyboard's reach too. */
1511
+ test("is inert, and answers again when the write ends", () => {
1512
+ const {editor, remote, host} = build("r16k1", "db/users");
1513
+ const $body = $in(editor, ".SCHEMA_BODY");
1514
+ expect($body.hasAttribute("inert")).toBe(false);
1515
+
1516
+ const write = start_a_write(editor, host, "name");
1517
+ expect($body.hasAttribute("inert")).toBe(true);
1518
+
1519
+ answer(editor, remote, write, 0, write.kw.record);
1520
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1521
+ expect($in(editor, ".SCHEMA_BODY").hasAttribute("inert")).toBe(false);
1522
+ expect(errors()).toEqual([]);
1523
+ });
1524
+
1525
+ test("a drop that ends the write gives the body back too", () => {
1526
+ const {editor, remote, host} = build("r16k2", "db/users");
1527
+ start_a_write(editor, host, "name");
1528
+ drop(editor, remote, host);
1529
+ expect($in(editor, ".SCHEMA_BODY").hasAttribute("inert")).toBe(false);
1530
+ });
1531
+ });
1532
+
1533
+ describe("the writes of an import mark the topic they write", () => {
1534
+
1535
+ /* An import that adds a topic, `roles`. */
1536
+ const IMPORT = JSON.stringify({id: "db", topics: [
1537
+ {id: "users", pkey: "id", cols: [
1538
+ {id: "id", type: "string", flag: ["persistent", "required"]}
1539
+ ]},
1540
+ {id: "roles", pkey: "id", cols: [
1541
+ {id: "id", type: "string", flag: ["persistent", "required"]}
1542
+ ]}
1543
+ ]});
1544
+
1545
+ /* The record as the store answers it: with the id it gave a new
1546
+ * one, and its fkeys in list_dict. */
1547
+ function as_the_store_answers(write)
1548
+ {
1549
+ const record = JSON.parse(JSON.stringify(write.kw.record));
1550
+ const to_dict = (refs) => refs.map((ref) => {
1551
+ const [topic_name, id, hook_name] = ref.split("^");
1552
+ return {topic_name, id, hook_name};
1553
+ });
1554
+ if(write.kw.topic_name === "topics") {
1555
+ record.id = record.id || `db.${record.value}`;
1556
+ record.treedbs = to_dict(record.treedbs || []);
1557
+ }
1558
+ if(write.kw.topic_name === "cols") {
1559
+ record.id = record.id || `${record.topics[0].split("^")[1]}.${record.value}`;
1560
+ record.topics = to_dict(record.topics || []);
1561
+ }
1562
+ return record;
1563
+ }
1564
+
1565
+ function run_the_import(editor, host)
1566
+ {
1567
+ gobj_send_event(editor, "EV_IMPORT", {}, host);
1568
+ gobj_send_event(editor, "EV_PREVIEW_IMPORT", {prune: false, text: IMPORT,
1569
+ model_gen: editor.priv.model_gen}, editor);
1570
+ expect(editor.priv.import_plan.writes.length).toBeGreaterThan(0);
1571
+ gobj_send_event(editor, "EV_APPLY_IMPORT", {model_gen: editor.priv.model_gen}, editor);
1572
+ expect(gobj_current_state(editor)).toBe("ST_SAVING");
1573
+ }
1574
+
1575
+ function answer_every_write(editor, remote)
1576
+ {
1577
+ for(let i = 0; i < 10; i++) {
1578
+ const [write] = take("update-node");
1579
+ if(!write) {
1580
+ break;
1581
+ }
1582
+ answer(editor, remote, write, 0, as_the_store_answers(write));
1583
+ }
1584
+ }
1585
+
1586
+ test("from another topic's screen: the topic it wrote, not the one on screen", () => {
1587
+ const {editor, remote, host} = build("r16m1", "db/users");
1588
+ run_the_import(editor, host);
1589
+ answer_every_write(editor, remote);
1590
+ expect(gobj_current_state(editor)).toBe("ST_COLUMNS");
1591
+ expect(editor.priv.written).toEqual({"db.roles": true});
1592
+ expect(errors()).toEqual([]);
1593
+ });
1594
+
1595
+ test("from the topics screen, where no topic is on screen", () => {
1596
+ const {editor, remote, host} = build("r16m2", "db");
1597
+ run_the_import(editor, host);
1598
+ answer_every_write(editor, remote);
1599
+ expect(gobj_current_state(editor)).toBe("ST_TOPICS");
1600
+ expect(editor.priv.written).toEqual({"db.roles": true});
1601
+ expect(errors()).toEqual([]);
1602
+ });
1603
+
1604
+ test("a column answered with no record: the record it queued says the topic", () => {
1605
+ const {editor, remote, host} = build("r16m3", "db/users");
1606
+ run_the_import(editor, host);
1607
+ const [topic] = take("update-node");
1608
+ expect(topic.kw.topic_name).toBe("topics");
1609
+ answer(editor, remote, topic, 0, as_the_store_answers(topic));
1610
+ const [col] = take("update-node");
1611
+ expect(col.kw.topic_name).toBe("cols");
1612
+ answer(editor, remote, col, 0, null);
1613
+ expect(editor.priv.written).toEqual({"db.roles": true});
1614
+ });
1615
+ });
1616
+
1617
+ describe("the reason a load failed", () => {
1618
+
1619
+ test("is a key the notice translates, not a string frozen in one language", () => {
1620
+ const remote = gobj_create_service("r16e1_remote", "C_TEST_REMOTE", {}, yuno);
1621
+ gobj_change_state(remote, "ST_SESSION");
1622
+ const host = gobj_create("r16e1_host", "C_TEST_EDITOR_HOST", {}, yuno);
1623
+ const editor = gobj_create("r16e1_editor", "C_YUI_SCHEMA_EDITOR", {
1624
+ gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
1625
+ }, host);
1626
+ gobj_start(editor);
1627
+ for(const c of take("nodes")) {
1628
+ answer(editor, remote, c, -1, null, "");
1629
+ }
1630
+ expect(gobj_current_state(editor)).toBe("ST_IDLE");
1631
+ expect(editor.priv.load_error).toBe("cannot load the schemas");
1632
+ /* The notice already says it: no detail repeating the title. */
1633
+ expect($in(editor, ".SCHEMA_NOTICE_DETAIL")).toBe(null);
1634
+ });
1635
+
1636
+ test("the backend's own words travel as its key", () => {
1637
+ const remote = gobj_create_service("r16e2_remote", "C_TEST_REMOTE", {}, yuno);
1638
+ gobj_change_state(remote, "ST_SESSION");
1639
+ const host = gobj_create("r16e2_host", "C_TEST_EDITOR_HOST", {}, yuno);
1640
+ const editor = gobj_create("r16e2_editor", "C_YUI_SCHEMA_EDITOR", {
1641
+ gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
1642
+ }, host);
1643
+ gobj_start(editor);
1644
+ for(const c of take("nodes")) {
1645
+ answer(editor, remote, c, -1, null, "treedb not found");
1646
+ }
1647
+ const $detail = $in(editor, ".SCHEMA_NOTICE_DETAIL");
1648
+ expect($detail.getAttribute("data-i18n")).toBe("treedb not found");
1649
+ });
1650
+
1651
+ test("a load that cannot leave keeps its key", () => {
1652
+ const remote = gobj_create_service("r16e3_remote", "C_TEST_REMOTE", {}, yuno);
1653
+ const host = gobj_create("r16e3_host", "C_TEST_EDITOR_HOST", {}, yuno);
1654
+ const editor = gobj_create("r16e3_editor", "C_YUI_SCHEMA_EDITOR", {
1655
+ gobj_remote_yuno: remote, treedb_name: "treedb_system_schema"
1656
+ }, host);
1657
+ gobj_start(editor);
1658
+ gobj_change_state(remote, "ST_DISCONNECTED");
1659
+ gobj_send_event(editor, "EV_REFRESH", {}, host);
1660
+ expect(editor.priv.load_error).toBe("cannot reach the treedb");
1661
+ const $detail = $in(editor, ".SCHEMA_NOTICE_DETAIL");
1662
+ expect($detail.getAttribute("data-i18n")).toBe("cannot reach the treedb");
1663
+ });
1664
+ });
@@ -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();
@@ -149,13 +167,19 @@ function save_arrangement(engine, topic)
149
167
  });
150
168
  }
151
169
 
152
- function answer(host, remote, request, result, comment)
170
+ /* The answer as the real transports build it: the command frame
171
+ * carries ONLY the request's `__md_command__` (C_IEVENT_CLI's
172
+ * mt_command, gui_agent's C_AGENT_TREEDB_LINK), never the request's
173
+ * own kw. Echoing the whole kw here once hid that the refusal read
174
+ * its topic from a `record` no real answer carries. */
175
+ function answer(host, remote, request, result, comment, data)
153
176
  {
177
+ const md_command = request.kw.__md_command__ || {};
154
178
  gobj_send_event(host, "EV_MT_COMMAND_ANSWER", {
155
179
  result: result,
156
180
  comment: comment || "",
157
- data: null,
158
- __md_iev__: {command_stack: [{command: request.command, kw: request.kw}]}
181
+ data: (data === undefined) ? null : data,
182
+ __md_iev__: {command_stack: [{command: request.command, kw: md_command}]}
159
183
  }, remote);
160
184
  }
161
185
 
@@ -172,6 +196,7 @@ describe("a __graphs__ write that does not land is said to the engine", () => {
172
196
  expect(commands.length).toBe(1);
173
197
  answer(host, remote, commands[0], -1, "not authorized");
174
198
  expect(refused()).toEqual(["yunos"]);
199
+ expect(logged.filter((l) => /names no topic/.test(l.msg))).toEqual([]);
175
200
  });
176
201
 
177
202
  test("landed: nothing is said", () => {
@@ -206,3 +231,53 @@ describe("a __graphs__ write that does not land is said to the engine", () => {
206
231
  expect(refused()).toEqual([]);
207
232
  });
208
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
+ });