@yuneta/gobj-ui 3.0.0 → 5.0.0

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.
Files changed (45) hide show
  1. package/README.md +335 -6
  2. package/dist/gobj-ui.cjs.js +10930 -5027
  3. package/dist/gobj-ui.es.js +10948 -5080
  4. package/index.js +45 -1
  5. package/package.json +11 -11
  6. package/src/c_g6_nodes_tree.js +128 -40
  7. package/src/c_yui_form.js +112 -53
  8. package/src/c_yui_gobj_tree_js.js +58 -36
  9. package/src/c_yui_json.css +139 -0
  10. package/src/c_yui_json.js +928 -0
  11. package/src/c_yui_json_graph.js +110 -20
  12. package/src/c_yui_map.js +19 -37
  13. package/src/c_yui_nav.js +6 -9
  14. package/src/c_yui_period.css +184 -0
  15. package/src/c_yui_period.js +1441 -0
  16. package/src/c_yui_shell.css +33 -0
  17. package/src/c_yui_shell.js +672 -111
  18. package/src/c_yui_treedb_graph.js +639 -37
  19. package/src/c_yui_treedb_schema.js +478 -0
  20. package/src/c_yui_treedb_topic_with_form.css +7 -42
  21. package/src/c_yui_treedb_topic_with_form.js +152 -103
  22. package/src/c_yui_treedb_topics.css +73 -0
  23. package/src/c_yui_treedb_topics.js +1070 -29
  24. package/src/c_yui_window.js +180 -97
  25. package/src/c_yui_window_manager.js +38 -4
  26. package/src/json_view_helpers.js +214 -0
  27. package/src/json_view_helpers.test.js +135 -0
  28. package/src/route_map_model.js +317 -0
  29. package/src/route_map_model.test.js +209 -0
  30. package/src/route_resolver.js +24 -1
  31. package/src/route_resolver.test.js +40 -1
  32. package/src/shell_modals.js +117 -39
  33. package/src/shell_route_map.css +225 -0
  34. package/src/shell_route_map.js +363 -0
  35. package/src/tabulator.css +67 -0
  36. package/src/yui_dev.js +130 -8
  37. package/src/yui_frontend_view.js +106 -0
  38. package/src/yui_icons.css +62 -0
  39. package/src/yui_inputs.css +8 -3
  40. package/src/yui_inputs.js +51 -8
  41. package/src/yui_tabulator_i18n.js +128 -0
  42. package/src/yui_theme.js +147 -0
  43. package/src/yui_time.js +666 -0
  44. package/src/yui_time.test.js +330 -0
  45. package/src/yui_toolbar.js +86 -43
@@ -57,6 +57,11 @@ import {
57
57
  trace_json,
58
58
  json_object_update_missing,
59
59
  gobj_start,
60
+ gobj_stop,
61
+ gobj_is_running,
62
+ gobj_current_state,
63
+ is_gobj,
64
+ gobj_is_destroying,
60
65
  log_info,
61
66
  trace_msg,
62
67
  refresh_language,
@@ -80,8 +85,8 @@ import {
80
85
  set_submit_state,
81
86
  set_active_state,
82
87
  } from "./lib_graph.js";
83
- import {yui_shell_show_error} from "./shell_modals.js";
84
- import {yui_shell_of} from "./c_yui_shell.js";
88
+ import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
89
+ import {yui_shell_of, yui_shell_set_sub_routes} from "./c_yui_shell.js";
85
90
 
86
91
  import {t} from "i18next";
87
92
 
@@ -110,6 +115,8 @@ SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote Yuno to
110
115
  SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote service treedb name"),
111
116
  SDATA(data_type_t.DTP_DICT, "descs", 0, null, "Descriptions of topics obtained"),
112
117
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
118
+ SDATA(data_type_t.DTP_STRING, "back_route", 0, "", "Optional hash route for a '← topics' button back to the topics grid (host-supplied; empty = no button)"),
119
+ SDATA(data_type_t.DTP_STRING, "base_route", 0, "", "This view's base route (host-supplied); used to declare its per-topic focus sub-routes to the site map (ROUTING.md contributor)."),
113
120
  SDATA(data_type_t.DTP_DICT, "records", 0, "{}", "Data of topics"),
114
121
  SDATA(data_type_t.DTP_LIST, "topics", 0, "[]", "List of topic objects"),
115
122
 
@@ -141,11 +148,15 @@ let PRIVATE_DATA = {
141
148
  gobj_nodes_tree: null,
142
149
  gobj_treedb_tables: null,
143
150
  hook_data_viewer: null,
151
+ json_gobj: null, /* C_YUI_JSON viewer of the raw tranger (or null) */
152
+ json_win: null, /* C_YUI_WINDOW hosting it, desktop (or null) */
153
+ json_modal: null, /* shell modal hosting it, mobile (or null) */
144
154
  with_treedb_tables: false,
145
155
  canvas_id: null,
146
156
  operation_mode: null,
147
157
  layout: null,
148
158
  _topics_subscribed: {},
159
+ _links_subscribed: false, /* EV_TREEDB_NODE_LINKED/UNLINKED (treedb-wide) */
149
160
 
150
161
  is_pinhold_window: false, // inherited of v6, todo review
151
162
  };
@@ -182,15 +193,6 @@ function mt_create(gobj)
182
193
  log_error(`${gobj_name(gobj)} -> treedb_name not configured`);
183
194
  }
184
195
 
185
- /* Optional legacy integration: EV_RESIZE from C_YUI_MAIN
186
- * (old shell). Under the new C_YUI_SHELL there is no
187
- * __yui_main__ — look it up SILENTLY (no verbose) so its
188
- * absence is not logged as an error every mount. */
189
- let __yui_main__ = gobj_find_service("__yui_main__");
190
- if(__yui_main__) {
191
- gobj_subscribe_event(__yui_main__, "EV_RESIZE", {}, gobj);
192
- }
193
-
194
196
  /*
195
197
  * set canvas_id, before build_ui()
196
198
  */
@@ -284,6 +286,11 @@ function mt_start(gobj)
284
286
  ***************************************************************/
285
287
  function mt_stop(gobj)
286
288
  {
289
+ let shell = yui_shell_of(gobj);
290
+ if(shell) {
291
+ yui_shell_set_sub_routes(shell, gobj_read_str_attr(gobj, "base_route"), null);
292
+ }
293
+ close_json_viewer(gobj);
287
294
  gobj_stop_children(gobj);
288
295
  }
289
296
 
@@ -318,12 +325,17 @@ function build_ui(gobj)
318
325
  let padding = gobj_read_attr(gobj, "padding");
319
326
  let $toolbar = make_toolbar(gobj);
320
327
 
328
+ /* The back link is pinned in the row, BEFORE the toolbar, so the
329
+ * toolbar's horizontal scroll never carries it off-screen. */
330
+ let $back = make_back_to_topics(gobj);
331
+ let row_items = $back ? [$back, $toolbar] : [$toolbar];
332
+
321
333
  let $container = createElement2(
322
334
  // Don't use is-flex, don't work well with is-hidden
323
335
  ['div', {class: 'C_YUI_TREEDB_GRAPH', style: `height:100%; display:flex; flex-direction:column;`}, [
324
- ['div', {class: 'is-flex-grow-0 is-flex is-align-items-center toolbar_yui_treedb_graph'}, $toolbar],
325
- ['div', {class: `is-flex-grow-1 ${padding}`, style: 'height:100%; min-height:0; overflow:hidden;'}, [
326
- ['div', {id: priv.canvas_id, class: `graph-container`, style: 'height:100%; min-height:0;border: 1px solid var(--bulma-border-weak);border-radius:0.2rem;'}, [
336
+ ['div', {class: 'GRAPH_TOOLBAR_ROW is-flex-grow-0 is-flex is-align-items-center'}, row_items],
337
+ ['div', {class: `GRAPH_BODY is-flex-grow-1 ${padding}`, style: 'height:100%; min-height:0; overflow:hidden;'}, [
338
+ ['div', {id: priv.canvas_id, class: `GRAPH_CANVAS graph-container`, style: 'height:100%; min-height:0;border: 1px solid var(--bulma-border-weak);border-radius:0.2rem;'}, [
327
339
  ]]
328
340
  ]]
329
341
  ]]
@@ -331,9 +343,49 @@ function build_ui(gobj)
331
343
 
332
344
  gobj_write_attr(gobj, "$container", $container);
333
345
  refresh_language($container, t);
346
+ refresh_raw_json_button(gobj); /* disabled until the backend session is up */
334
347
  return $container;
335
348
  }
336
349
 
350
+ /************************************************************
351
+ * The "Raw JSON" viewer issues a remote print-tranger, so it only makes
352
+ * sense with a live backend session — the remote (C_IEVENT_CLI) is in
353
+ * ST_SESSION exactly while connected. Enable the button only then; native
354
+ * `disabled` dims it (Bulma) and blocks its click.
355
+ ************************************************************/
356
+ function is_connected(gobj)
357
+ {
358
+ let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
359
+ return !!remote && gobj_current_state(remote) === "ST_SESSION";
360
+ }
361
+
362
+ function refresh_raw_json_button(gobj, connected)
363
+ {
364
+ let $container = gobj_read_attr(gobj, "$container");
365
+ if(!$container) {
366
+ return;
367
+ }
368
+ if(typeof connected !== "boolean") {
369
+ connected = is_connected(gobj);
370
+ }
371
+ let $raw = $container.querySelector(".TREEDB_JSON_BTN");
372
+ if($raw) {
373
+ $raw.disabled = !connected;
374
+ }
375
+ }
376
+
377
+ /************************************************************
378
+ * The host (C_TREEDB_VIEW) forwards the backend transport edges here so the
379
+ * "Raw JSON" button disables the moment the session drops and re-enables on
380
+ * reconnect. The library view must not subscribe to the C_IEVENT_CLI itself
381
+ * (that forwards the subscription upstream and breaks the session).
382
+ ************************************************************/
383
+ function ac_transport_state(gobj, event, kw, src)
384
+ {
385
+ refresh_raw_json_button(gobj, !!(kw && kw.connected));
386
+ return 0;
387
+ }
388
+
337
389
  /************************************************************
338
390
  * Show a non-blocking inline error banner at the top of the view
339
391
  * (used when the treedb schema `descs` cannot load). Reuses a single
@@ -345,10 +397,10 @@ function show_load_error(gobj, message)
345
397
  if(!$container) {
346
398
  return;
347
399
  }
348
- let $err = $container.querySelector(".treedb-load-error");
400
+ let $err = $container.querySelector(".GRAPH_LOAD_ERROR");
349
401
  if(!$err) {
350
402
  $err = createElement2(
351
- ["div", {class: "notification is-danger is-light m-3 treedb-load-error"}, []]
403
+ ["div", {class: "GRAPH_LOAD_ERROR notification is-danger is-light m-3"}, []]
352
404
  );
353
405
  $container.insertBefore($err, $container.firstChild);
354
406
  }
@@ -389,9 +441,9 @@ function make_toolbar(gobj)
389
441
  * via populate_nodes_tree_options()
390
442
  */
391
443
  let left_items = [
392
- ['span', {class: 'is-hidden-mobile', style: 'padding-right:5px;', i18n: 'layout'}, 'layout'],
444
+ ['span', {class: 'GRAPH_LAYOUT_LABEL is-hidden-mobile', style: 'padding-right:5px;', i18n: 'layout'}, 'layout'],
393
445
  ['div', {class: 'select'}, [
394
- ['select', {class: 'graph_layout'}]
446
+ ['select', {class: 'GRAPH_LAYOUT_SELECT'}]
395
447
  ], {
396
448
  change: (evt) => {
397
449
  evt.stopPropagation();
@@ -399,9 +451,9 @@ function make_toolbar(gobj)
399
451
  }
400
452
  }],
401
453
 
402
- ['span', {class: 'is-hidden-mobile', style: 'padding-left:10px; padding-right:5px;', i18n: 'operation mode'}, 'operation mode'],
454
+ ['span', {class: 'GRAPH_MODE_LABEL is-hidden-mobile', style: 'padding-left:10px; padding-right:5px;', i18n: 'operation mode'}, 'operation mode'],
403
455
  ['div', {class: 'select'}, [
404
- ['select', {class: 'graph_operation_mode'}, mode_options]
456
+ ['select', {class: 'GRAPH_MODE_SELECT'}, mode_options]
405
457
  ], {
406
458
  change: (evt) => {
407
459
  evt.stopPropagation();
@@ -409,7 +461,7 @@ function make_toolbar(gobj)
409
461
  }
410
462
  }],
411
463
 
412
- ['button', {class: 'button EV_REFRESH_TREEDB'}, [
464
+ ['button', {class: 'GRAPH_REFRESH button'}, [
413
465
  ['i', {class: 'yi-arrows-rotate'}],
414
466
  ['span', {class: 'is-hidden-mobile', style: 'padding-left:5px;', i18n: 'refresh'}, 'refresh']
415
467
  ], {
@@ -418,6 +470,21 @@ function make_toolbar(gobj)
418
470
  gobj_send_event(gobj, "EV_REFRESH_TREEDB", {evt}, gobj);
419
471
  }
420
472
  }],
473
+
474
+ /* Inspect the treedb's raw tranger json in the lazy tree viewer
475
+ * (print-tranger on the C_NODE service). A treedb can be huge, so
476
+ * the viewer drills in on demand — see open_json_viewer. */
477
+ ['button', {class: 'button ml-2 TREEDB_JSON_BTN',
478
+ title: t('raw json'), 'aria-label': t('raw json'),
479
+ 'data-i18n-title': 'raw json', 'data-i18n-aria-label': 'raw json'}, [
480
+ ['i', {class: 'yi-eye'}],
481
+ ['span', {class: 'is-hidden-mobile', style: 'padding-left:5px;', i18n: 'raw json'}, 'raw json']
482
+ ], {
483
+ click: (evt) => {
484
+ evt.stopPropagation();
485
+ gobj_send_event(gobj, "EV_OPEN_JSON", {}, gobj);
486
+ }
487
+ }],
421
488
  ];
422
489
 
423
490
  /*
@@ -434,7 +501,7 @@ function make_toolbar(gobj)
434
501
  const $toolbar_header = yui_toolbar({}, [
435
502
  ['div', {class: 'yui-horizontal-toolbar-section left'}, left_items],
436
503
  ['div', {class: 'yui-horizontal-toolbar-section center'}, center_items],
437
- ['div', {class: 'yui-horizontal-toolbar-section right mode_buttons'}, right_items]
504
+ ['div', {class: 'GRAPH_MODE_BUTTONS yui-horizontal-toolbar-section right'}, right_items]
438
505
  ]);
439
506
 
440
507
  refresh_language($toolbar_header, t);
@@ -442,6 +509,242 @@ function make_toolbar(gobj)
442
509
  return $toolbar_header;
443
510
  }
444
511
 
512
+ /************************************************************
513
+ * "← topics": a real hash link back to the topics grid (host-supplied
514
+ * `back_route`), shown only when set. Lets a graph reached from a topic
515
+ * card's graph icon return to the cards landing — symmetric with the
516
+ * topics view's own back button. Absent (e.g. wattyzer) ⇒ no button.
517
+ *
518
+ * It is deliberately NOT a toolbar item: yui_toolbar() lays its items in
519
+ * a horizontally SCROLLING container, and this is the only control here
520
+ * that leaves the view instead of acting on the graph — a way out cannot
521
+ * depend on where the toolbar happens to be scrolled. It sits pinned
522
+ * ahead of the toolbar, on the same row (see build_ui), like the topics
523
+ * view's back button, whose plain non-scrolling strip never moves.
524
+ ************************************************************/
525
+ function make_back_to_topics(gobj)
526
+ {
527
+ let back_route = gobj_read_attr(gobj, "back_route") || "";
528
+ if(!back_route) {
529
+ return null;
530
+ }
531
+
532
+ let $back = createElement2(
533
+ ['a', {class: 'GRAPH_BACK_TOPICS button ml-1 mr-1 is-flex-shrink-0',
534
+ href: back_route,
535
+ title: t('topics'), 'aria-label': t('topics'),
536
+ 'data-i18n-title': 'topics', 'data-i18n-aria-label': 'topics'}, [
537
+ ['span', {class: 'icon'}, [['i', {class: 'yi-arrow-left'}]]],
538
+ ['span', {class: 'is-hidden-mobile', style: 'padding-left:5px;',
539
+ i18n: 'topics'}, 'topics']
540
+ ]]
541
+ );
542
+ refresh_language($back, t);
543
+
544
+ return $back;
545
+ }
546
+
547
+ /************************************************************
548
+ * True on a phone-width viewport (Bulma's mobile breakpoint).
549
+ ************************************************************/
550
+ function is_mobile()
551
+ {
552
+ return typeof window !== "undefined" && window.innerWidth <= 768;
553
+ }
554
+
555
+ /************************************************************
556
+ * Raw-tranger JSON viewer: a C_YUI_JSON driving its own DOM, hosted in a
557
+ * moveable C_YUI_WINDOW on desktop / an adaptive modal sheet on mobile. It
558
+ * is a helper of THIS view (CHILD model: it publishes EV_EXPAND_PATH back
559
+ * to us), single at a time.
560
+ *
561
+ * A treedb's tranger can be enormous, so the first fetch is collapsed
562
+ * (print-tranger with lists/dicts limits) and the viewer drills in on
563
+ * demand: EV_EXPAND_PATH -> print-tranger path=<path> -> EV_SUBTREE_LOADED.
564
+ *
565
+ * print-tranger must exist on the backend C_NODE service (SDK >= the
566
+ * release that added it); against an older backend the command answers
567
+ * "command not found" and the viewer shows that error.
568
+ ************************************************************/
569
+ function open_json_viewer(gobj)
570
+ {
571
+ let priv = gobj.priv;
572
+ if(priv.json_win || priv.json_modal) {
573
+ return; /* already open */
574
+ }
575
+ if(gclass_find_by_name("C_YUI_JSON") === null) {
576
+ log_error(`${gobj_short_name(gobj)}: C_YUI_JSON not registered by the app`);
577
+ yui_shell_show_error(yui_shell_of(gobj), "raw json viewer unavailable", {t: t});
578
+ return;
579
+ }
580
+
581
+ let mobile = is_mobile();
582
+ let shell = yui_shell_of(gobj);
583
+
584
+ let jv = gobj_create_service(
585
+ `treedb-json-${clean_name(gobj_name(gobj))}`,
586
+ "C_YUI_JSON",
587
+ {
588
+ /* No `title`: the host titles it — the window's title bar on
589
+ * desktop, the dialog's header on mobile. The viewer's own
590
+ * title would land INSIDE that host, doubling it. */
591
+ subscriber: gobj /* publishes EV_EXPAND_PATH to us */
592
+ },
593
+ gobj
594
+ );
595
+ if(!jv) {
596
+ log_error(`${gobj_short_name(gobj)}: cannot create the JSON viewer`);
597
+ return;
598
+ }
599
+ priv.json_gobj = jv;
600
+ gobj_start(jv);
601
+ let $box = gobj_read_pointer_attr(jv, "$container");
602
+
603
+ if(mobile) {
604
+ if(!shell) {
605
+ log_error(`${gobj_short_name(gobj)}: no shell, cannot open the JSON sheet`);
606
+ close_json_viewer(gobj);
607
+ return;
608
+ }
609
+ priv.json_modal = yui_shell_show_modal(shell, $box, {
610
+ dialog: true,
611
+ logical_class: "TREEDB_JSON_SHEET",
612
+ title_prefix: priv.treedb_name,
613
+ title: "raw json",
614
+ t: t,
615
+ on_close: () => {
616
+ if(gobj_is_destroying(gobj)) {
617
+ return;
618
+ }
619
+ gobj_send_event(gobj, "EV_JSON_CLOSED", {}, gobj);
620
+ }
621
+ });
622
+ } else {
623
+ let $win_parent = (shell && yui_shell_popup_layer(shell)) ||
624
+ (typeof document !== "undefined" && document.getElementById("top-layer")) ||
625
+ null;
626
+
627
+ priv.json_win = gobj_create_service(
628
+ `treedb-jsonwin-${clean_name(gobj_name(gobj))}`,
629
+ "C_YUI_WINDOW",
630
+ {
631
+ $parent: $win_parent,
632
+ subscriber: null,
633
+ modal: false,
634
+ showMax: true,
635
+ showFooter: false,
636
+ resizable: true,
637
+ center: true,
638
+ auto_save_size_and_position: true,
639
+ width: 640,
640
+ height: 620,
641
+ logical_class: "TREEDB_JSON_WINDOW",
642
+ title_prefix: priv.treedb_name,
643
+ title: "raw json",
644
+ icon: "yi-eye",
645
+ body: $box,
646
+ manager: null,
647
+ on_close: () => {
648
+ if(gobj_is_destroying(gobj)) {
649
+ return;
650
+ }
651
+ gobj_send_event(gobj, "EV_JSON_CLOSED", {}, gobj);
652
+ }
653
+ },
654
+ gobj
655
+ );
656
+ if(!priv.json_win) {
657
+ log_error(`${gobj_short_name(gobj)}: cannot create the JSON window`);
658
+ close_json_viewer(gobj);
659
+ return;
660
+ }
661
+ gobj_start(priv.json_win);
662
+ }
663
+
664
+ request_print_tranger(gobj, ""); /* first fetch: whole tranger, collapsed */
665
+ }
666
+
667
+ /************************************************************
668
+ * Close the JSON viewer (user dismiss / teardown). Destroys the viewer
669
+ * gobj and whichever presenter is up, then clears the refs.
670
+ ************************************************************/
671
+ function close_json_viewer(gobj)
672
+ {
673
+ let priv = gobj.priv;
674
+ let jv = priv.json_gobj;
675
+ let win = priv.json_win;
676
+ let modal = priv.json_modal;
677
+
678
+ priv.json_gobj = null;
679
+ priv.json_win = null;
680
+ priv.json_modal = null;
681
+
682
+ if(win && is_gobj(win)) {
683
+ try {
684
+ /* STOP, then destroy — same rule as the viewer below: the
685
+ * window was STARTED on open, and gobj_destroy() raises the
686
+ * `destroying` flag before it can stop a running gobj, so
687
+ * destroying it straight logs two errors and skips mt_stop.
688
+ * The ✕ path already stopped it (close_window) — guard. */
689
+ if(gobj_is_running(win)) {
690
+ gobj_stop(win);
691
+ }
692
+ gobj_destroy(win);
693
+ } catch(e) {
694
+ log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);
695
+ }
696
+ }
697
+ if(modal && typeof modal.close === "function") {
698
+ try {
699
+ modal.close();
700
+ } catch(e) {
701
+ log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);
702
+ }
703
+ }
704
+ if(jv && is_gobj(jv)) {
705
+ try {
706
+ /* STOP, then destroy — the viewer was STARTED in open_json_viewer.
707
+ * gobj_destroy() raises the `destroying` flag before it can stop a
708
+ * running gobj, so destroying it straight logs "Destroying a
709
+ * RUNNING gobj" + "gobj NULL or DESTROYED" and skips its mt_stop. */
710
+ gobj_stop(jv);
711
+ gobj_destroy(jv);
712
+ } catch(e) {
713
+ log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);
714
+ }
715
+ }
716
+ }
717
+
718
+ /************************************************************
719
+ * Fetch the treedb's raw tranger (or one subtree when `path` is set) as
720
+ * bounded, drillable JSON. Collapsed at 100 so a huge tranger stays a
721
+ * small payload of `__collapsed__` stubs the viewer expands on demand.
722
+ ************************************************************/
723
+ function request_print_tranger(gobj, path)
724
+ {
725
+ let priv = gobj.priv;
726
+ if(!priv.gobj_remote_yuno) {
727
+ log_error(`${gobj_short_name(gobj)}: No gobj_remote_yuno defined`);
728
+ let jv = priv.json_gobj;
729
+ if(path && jv && is_gobj(jv) && !gobj_is_destroying(jv)) {
730
+ gobj_send_event(jv, "EV_SUBTREE_ERROR",
731
+ {path: path, error: t("no session")}, gobj);
732
+ }
733
+ return;
734
+ }
735
+ let ret = gobj_command(priv.gobj_remote_yuno, "print-tranger",
736
+ {
737
+ service: priv.treedb_name,
738
+ expanded: 1,
739
+ lists_limit: 100,
740
+ dicts_limit: 100,
741
+ path: path || ""
742
+ }, gobj);
743
+ if(ret) {
744
+ log_error(ret);
745
+ }
746
+ }
747
+
445
748
  /************************************************************
446
749
  * Populate layout dropdown from child's available layouts
447
750
  ************************************************************/
@@ -451,7 +754,7 @@ function populate_nodes_tree_options(gobj)
451
754
  let $container = priv.$container;
452
755
 
453
756
  let layout_names = gobj_read_attr(priv.gobj_nodes_tree, "layout_names");
454
- let $layout_select = $container.querySelector('.graph_layout');
757
+ let $layout_select = $container.querySelector('.GRAPH_LAYOUT_SELECT');
455
758
  if($layout_select && layout_names) {
456
759
  for(let name of layout_names) {
457
760
  let option = document.createElement('option');
@@ -467,7 +770,7 @@ function populate_nodes_tree_options(gobj)
467
770
  }
468
771
 
469
772
  // Restore persisted operation_mode selection
470
- let $operation_mode_select = $container.querySelector('.graph_operation_mode');
773
+ let $operation_mode_select = $container.querySelector('.GRAPH_MODE_SELECT');
471
774
  $operation_mode_select.value = priv.operation_mode;
472
775
  }
473
776
 
@@ -507,6 +810,48 @@ function treedb_nodes(gobj, treedb_name, topic_name, options)
507
810
  }
508
811
  }
509
812
 
813
+ /************************************************************
814
+ * Command to remote service: re-read ONE node (by id).
815
+ *
816
+ * Used to resync a node whose links changed: the answer is fed to the
817
+ * nodes-tree as EV_NODE_UPDATED, whose fkey diff (old vs new refs) is what
818
+ * draws or clears the edge. Reusing that path keeps the tree the single
819
+ * owner of the edge model — the alternative (deriving the edge here from
820
+ * the event's parent/child ids) would duplicate it.
821
+ ************************************************************/
822
+ function treedb_get_node(gobj, treedb_name, topic_name, node_id)
823
+ {
824
+ let priv = gobj.priv;
825
+
826
+ if(!priv.gobj_remote_yuno) {
827
+ log_error(`${gobj_short_name(gobj)}: No gobj_remote_yuno defined`);
828
+ return;
829
+ }
830
+
831
+ let kw = {
832
+ service: treedb_name,
833
+ treedb_name: treedb_name,
834
+ topic_name: topic_name,
835
+ node_id: node_id,
836
+ options: {
837
+ list_dict: true /* same shape as the initial `nodes` load */
838
+ }
839
+ };
840
+
841
+ kw.__md_command__ = { // Data to be returned
842
+ topic_name: topic_name,
843
+ };
844
+
845
+ let ret = gobj_command(priv.gobj_remote_yuno,
846
+ "node",
847
+ kw,
848
+ gobj
849
+ );
850
+ if(ret) {
851
+ log_error(ret);
852
+ }
853
+ }
854
+
510
855
  /************************************************************
511
856
  * Command to remote service
512
857
  ************************************************************/
@@ -754,6 +1099,36 @@ function process_treedb_descs(gobj, descs)
754
1099
  //create_combo(gobj, desc);
755
1100
  get_nodes(gobj, topic_name);
756
1101
  }
1102
+
1103
+ /* Declare per-topic focus sub-routes to the site map (ROUTING.md). */
1104
+ register_sub_routes(gobj);
1105
+ }
1106
+
1107
+ /************************************************************
1108
+ * Declare this graph's per-topic focus sub-routes to the site map.
1109
+ * Route-agnostic except for the host-supplied `base_route`. Cleared
1110
+ * on stop.
1111
+ ************************************************************/
1112
+ function register_sub_routes(gobj)
1113
+ {
1114
+ let shell = yui_shell_of(gobj);
1115
+ let base = gobj_read_str_attr(gobj, "base_route");
1116
+ if(!shell || !base) {
1117
+ return;
1118
+ }
1119
+ let descs = gobj_read_attr(gobj, "descs");
1120
+ let system = gobj_read_bool_attr(gobj, "system");
1121
+ let nodes = [];
1122
+ if(descs) {
1123
+ for(const topic of Object.keys(descs)) {
1124
+ if(!system && topic.substring(0, 2) === "__") {
1125
+ continue;
1126
+ }
1127
+ nodes.push({route: base + "/" + topic, label: topic,
1128
+ icon: "yi-hexagon-nodes", gclass: GCLASS_NAME});
1129
+ }
1130
+ }
1131
+ yui_shell_set_sub_routes(shell, base, nodes);
757
1132
  }
758
1133
 
759
1134
  /************************************************************
@@ -779,6 +1154,43 @@ function get_nodes(gobj, topic_name)
779
1154
  );
780
1155
  }
781
1156
 
1157
+ /************************************************************
1158
+ * EV_TREEDB_NODE_LINKED / EV_TREEDB_NODE_UNLINKED are TREEDB-wide, not
1159
+ * per-topic: their kw is the RELATIONSHIP
1160
+ * ({hook_name, parent_topic_name, child_topic_name, parent_id, child_id,
1161
+ * treedb_name}), with no `topic_name` — so they are subscribed ONCE, and
1162
+ * filtered by treedb_name alone (a {topic_name} filter would match nothing).
1163
+ *
1164
+ * NOTE: the backend only publishes them when its C_NODE service is
1165
+ * configured with `with_link_events` (SDF_RD, default FALSE). Without it,
1166
+ * a link/unlink is announced the backward-compatible way — an
1167
+ * EV_TREEDB_NODE_UPDATED of the PARENT — which cannot move an edge here:
1168
+ * an edge IS a fkey of the CHILD (link-saves-child), and the parent's fkeys
1169
+ * did not change. That is why an open Graph kept showing stale edges when
1170
+ * another operator linked two nodes.
1171
+ ************************************************************/
1172
+ function subscribe_treedb_links(gobj)
1173
+ {
1174
+ let priv = gobj.priv;
1175
+ if(priv._links_subscribed) {
1176
+ return;
1177
+ }
1178
+ priv._links_subscribed = true;
1179
+
1180
+ for(let event of ["EV_TREEDB_NODE_LINKED", "EV_TREEDB_NODE_UNLINKED"]) {
1181
+ gobj_subscribe_event(priv.gobj_remote_yuno,
1182
+ event,
1183
+ {
1184
+ __service__: priv.treedb_name,
1185
+ __filter__: {
1186
+ "treedb_name": priv.treedb_name
1187
+ }
1188
+ },
1189
+ gobj
1190
+ );
1191
+ }
1192
+ }
1193
+
782
1194
  /************************************************************
783
1195
  *
784
1196
  ************************************************************/
@@ -788,6 +1200,8 @@ function subscribe_treedb(gobj, topic_name)
788
1200
  const gobj_remote_yuno = priv.gobj_remote_yuno;
789
1201
  const treedb_name = priv.treedb_name;
790
1202
 
1203
+ subscribe_treedb_links(gobj);
1204
+
791
1205
  /*
792
1206
  * Avoid repetitions of subscribings
793
1207
  */
@@ -881,6 +1295,34 @@ function unsubscribe_treedb(gobj, topic_name)
881
1295
  },
882
1296
  gobj
883
1297
  );
1298
+
1299
+ /* The treedb-wide LINKED/UNLINKED subscription rides the per-topic
1300
+ * ones: when the last topic goes, it goes too — otherwise it outlives
1301
+ * every view of the treedb and keeps pushing events nobody will use
1302
+ * (the handler's _topics_subscribed guards make them no-ops, but the
1303
+ * traffic still crosses the wire). */
1304
+ let any_left = false;
1305
+ for(let k in priv._topics_subscribed) {
1306
+ if(priv._topics_subscribed[k]) {
1307
+ any_left = true;
1308
+ break;
1309
+ }
1310
+ }
1311
+ if(!any_left && priv._links_subscribed) {
1312
+ priv._links_subscribed = false;
1313
+ for(let ev of ["EV_TREEDB_NODE_LINKED", "EV_TREEDB_NODE_UNLINKED"]) {
1314
+ gobj_unsubscribe_event(gobj_remote_yuno,
1315
+ ev,
1316
+ {
1317
+ __service__: treedb_name,
1318
+ __filter__: {
1319
+ "treedb_name": treedb_name
1320
+ }
1321
+ },
1322
+ gobj
1323
+ );
1324
+ }
1325
+ }
884
1326
  }
885
1327
 
886
1328
  /********************************************
@@ -935,6 +1377,36 @@ function ac_mt_command_answer(gobj, event, kw, src)
935
1377
  let command = kw_get_str(gobj, __command__, "command", "", kw_flag_t.KW_REQUIRED);
936
1378
  let kw_command = kw_get_dict(gobj, __command__, "kw", {}, kw_flag_t.KW_REQUIRED);
937
1379
 
1380
+ /*
1381
+ * print-tranger feeds the raw-JSON viewer, correlated by the echoed
1382
+ * `path`: empty = first whole-tranger fetch (EV_SET_JSON), a set path =
1383
+ * a lazy drill (EV_SUBTREE_LOADED). Handled before the generic error
1384
+ * path so a failed drill marks its own branch, not the whole view.
1385
+ */
1386
+ if(command === "print-tranger") {
1387
+ let jv = priv.json_gobj;
1388
+ if(!jv || !is_gobj(jv) || gobj_is_destroying(jv)) {
1389
+ return 0; /* viewer closed before its answer landed: benign */
1390
+ }
1391
+ let path = kw_get_str(gobj, kw_command, "path", "", 0);
1392
+ if(result < 0) {
1393
+ if(path) {
1394
+ gobj_send_event(jv, "EV_SUBTREE_ERROR",
1395
+ {path: path, error: comment || "print-tranger failed"}, gobj);
1396
+ } else {
1397
+ yui_shell_show_error(yui_shell_of(gobj),
1398
+ comment || "print-tranger failed", {t: t});
1399
+ }
1400
+ return 0;
1401
+ }
1402
+ if(path) {
1403
+ gobj_send_event(jv, "EV_SUBTREE_LOADED", {path: path, json: data}, gobj);
1404
+ } else {
1405
+ gobj_send_event(jv, "EV_SET_JSON", {json: data}, gobj);
1406
+ }
1407
+ return 0;
1408
+ }
1409
+
938
1410
  if(result < 0) {
939
1411
  if(command === "descs") {
940
1412
  /* The schema couldn't load (not a treedb, no authz for it, backend
@@ -973,6 +1445,24 @@ function ac_mt_command_answer(gobj, event, kw, src)
973
1445
  }
974
1446
  break;
975
1447
 
1448
+ case "node":
1449
+ if(result >= 0) {
1450
+ /*
1451
+ * A node we re-read because its links changed. Feed it as an
1452
+ * UPDATE: the tree diffs its fkey refs against the ones it
1453
+ * holds and draws / clears exactly the edges that moved.
1454
+ */
1455
+ gobj_send_event(priv.gobj_nodes_tree,
1456
+ "EV_NODE_UPDATED",
1457
+ {
1458
+ topic_name: kw_get_str(gobj, kw_command, "topic_name", "", 0),
1459
+ node: data
1460
+ },
1461
+ gobj
1462
+ );
1463
+ }
1464
+ break;
1465
+
976
1466
  case "create-node":
977
1467
  case "update-node":
978
1468
  case "delete-node":
@@ -1049,6 +1539,51 @@ function ac_treedb_node_updated(gobj, event, kw, src)
1049
1539
  return 0;
1050
1540
  }
1051
1541
 
1542
+ /********************************************
1543
+ * Remote subscription response: two nodes were LINKED or UNLINKED
1544
+ * (by us, or by another operator on the same treedb).
1545
+ *
1546
+ * The kw is the relationship, not a node:
1547
+ * {hook_name, parent_topic_name, child_topic_name,
1548
+ * parent_id, child_id, treedb_name}
1549
+ *
1550
+ * An edge IS a fkey of the CHILD (link-saves-child), so the child is what
1551
+ * must be resynced — re-read it, and its EV_NODE_UPDATED diff moves the
1552
+ * edge. The PARENT is re-read too: its hook (the children list it shows,
1553
+ * and what the hook-data viewer reads) changed in memory even though it was
1554
+ * never saved.
1555
+ *
1556
+ * Only topics already loaded in the graph are re-read: a link to a topic
1557
+ * the user never opened has no node here to update.
1558
+ ********************************************/
1559
+ function ac_treedb_node_linked(gobj, event, kw, src)
1560
+ {
1561
+ let priv = gobj.priv;
1562
+ let treedb_name = kw_get_str(gobj, kw, "treedb_name", "", 0);
1563
+ let parent_topic_name = kw_get_str(gobj, kw, "parent_topic_name", "", 0);
1564
+ let child_topic_name = kw_get_str(gobj, kw, "child_topic_name", "", 0);
1565
+ let parent_id = kw_get_str(gobj, kw, "parent_id", "", 0);
1566
+ let child_id = kw_get_str(gobj, kw, "child_id", "", 0);
1567
+
1568
+ if(treedb_name !== priv.treedb_name) {
1569
+ log_error("It's not my treedb_name: " + treedb_name);
1570
+ return 0;
1571
+ }
1572
+ if(!child_id || !parent_id) {
1573
+ log_error(`${gobj_short_name(gobj)}: ${event} without parent_id/child_id`);
1574
+ return -1;
1575
+ }
1576
+
1577
+ if(priv._topics_subscribed[child_topic_name]) {
1578
+ treedb_get_node(gobj, treedb_name, child_topic_name, child_id);
1579
+ }
1580
+ if(priv._topics_subscribed[parent_topic_name]) {
1581
+ treedb_get_node(gobj, treedb_name, parent_topic_name, parent_id);
1582
+ }
1583
+
1584
+ return 0;
1585
+ }
1586
+
1052
1587
  /********************************************
1053
1588
  * Remote subscription response
1054
1589
  ********************************************/
@@ -1090,6 +1625,50 @@ function ac_refresh_treedb(gobj, event, kw, src)
1090
1625
  return 0;
1091
1626
  }
1092
1627
 
1628
+ /********************************************
1629
+ * Open the raw-tranger JSON viewer.
1630
+ ********************************************/
1631
+ function ac_open_json(gobj, event, kw, src)
1632
+ {
1633
+ open_json_viewer(gobj);
1634
+ return 0;
1635
+ }
1636
+
1637
+ /********************************************
1638
+ * The viewer asked to load a collapsed subtree: re-issue print-tranger
1639
+ * for that path. The answer returns through ac_mt_command_answer and is
1640
+ * fed back as EV_SUBTREE_LOADED / EV_SUBTREE_ERROR.
1641
+ ********************************************/
1642
+ function ac_json_expand_path(gobj, event, kw, src)
1643
+ {
1644
+ request_print_tranger(gobj, (kw && kw.path) || "");
1645
+ return 0;
1646
+ }
1647
+
1648
+ /********************************************
1649
+ * The JSON viewer was dismissed (X / dock / Escape / back), or torn down
1650
+ * by close_json_viewer(): release the viewer and clear the refs.
1651
+ ********************************************/
1652
+ function ac_json_closed(gobj, event, kw, src)
1653
+ {
1654
+ let priv = gobj.priv;
1655
+ let jv = priv.json_gobj;
1656
+ priv.json_gobj = null;
1657
+ priv.json_win = null;
1658
+ priv.json_modal = null;
1659
+ if(jv && is_gobj(jv)) {
1660
+ try {
1661
+ /* STOP before destroy — the viewer was STARTED in open_json_viewer
1662
+ * (see close_json_viewer for the full rationale). */
1663
+ gobj_stop(jv);
1664
+ gobj_destroy(jv);
1665
+ } catch(e) {
1666
+ log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);
1667
+ }
1668
+ }
1669
+ return 0;
1670
+ }
1671
+
1093
1672
  /********************************************
1094
1673
  * Event from G6_nodes_tree
1095
1674
  * kw: {
@@ -1399,7 +1978,7 @@ function ac_set_layout(gobj, event, kw, src)
1399
1978
  gobj_write_str_attr(gobj, "layout", layout);
1400
1979
  gobj_save_persistent_attrs(gobj, "layout");
1401
1980
 
1402
- let $layout_select = priv.$container.querySelector('.graph_layout');
1981
+ let $layout_select = priv.$container.querySelector('.GRAPH_LAYOUT_SELECT');
1403
1982
  $layout_select.value = priv.layout;
1404
1983
 
1405
1984
  gobj_send_event(
@@ -1425,7 +2004,7 @@ function ac_set_operation_mode(gobj, event, kw, src)
1425
2004
  gobj_write_str_attr(gobj, "operation_mode", operation_mode);
1426
2005
  gobj_save_persistent_attrs(gobj, "operation_mode");
1427
2006
 
1428
- let $operation_mode_select = priv.$container.querySelector('.graph_operation_mode');
2007
+ let $operation_mode_select = priv.$container.querySelector('.GRAPH_MODE_SELECT');
1429
2008
  $operation_mode_select.value = priv.operation_mode;
1430
2009
 
1431
2010
  gobj_send_event(
@@ -1448,6 +2027,26 @@ function ac_set_operation_mode(gobj, event, kw, src)
1448
2027
  return 0;
1449
2028
  }
1450
2029
 
2030
+ /************************************************************
2031
+ * Focus the graph on a topic (a topic card's graph icon, or a
2032
+ * `.../graphs/db/<sel>/<topic>` deep link): forward it to the G6
2033
+ * tree, which centres + highlights that topic's nodes (deferring
2034
+ * until its data is loaded).
2035
+ ************************************************************/
2036
+ function ac_set_focus_topic(gobj, event, kw, src)
2037
+ {
2038
+ let priv = gobj.priv;
2039
+ if(priv.gobj_nodes_tree) {
2040
+ gobj_send_event(
2041
+ priv.gobj_nodes_tree,
2042
+ "EV_FOCUS_TOPIC",
2043
+ {topic: kw && kw.topic},
2044
+ gobj
2045
+ );
2046
+ }
2047
+ return 0;
2048
+ }
2049
+
1451
2050
  /************************************************************
1452
2051
  * Parent (routing) inform us that we go showing
1453
2052
  *
@@ -1473,15 +2072,6 @@ function ac_hide(gobj, event, kw, src)
1473
2072
  return gobj_send_event(priv.gobj_nodes_tree, event, kw, gobj);
1474
2073
  }
1475
2074
 
1476
- /************************************************************
1477
- * we are subscribed to EV_RESIZE from __yui_main__
1478
- ************************************************************/
1479
- function ac_resize(gobj, event, kw, src)
1480
- {
1481
- let priv = gobj.priv;
1482
- return gobj_send_event(priv.gobj_nodes_tree, event, kw, gobj);
1483
- }
1484
-
1485
2075
 
1486
2076
 
1487
2077
 
@@ -1522,7 +2112,12 @@ function create_gclass(gclass_name)
1522
2112
  ["EV_TREEDB_NODE_CREATED", ac_treedb_node_created, null],
1523
2113
  ["EV_TREEDB_NODE_UPDATED", ac_treedb_node_updated, null],
1524
2114
  ["EV_TREEDB_NODE_DELETED", ac_treedb_node_deleted, null],
2115
+ ["EV_TREEDB_NODE_LINKED", ac_treedb_node_linked, null],
2116
+ ["EV_TREEDB_NODE_UNLINKED", ac_treedb_node_linked, null],
1525
2117
  ["EV_REFRESH_TREEDB", ac_refresh_treedb, null],
2118
+ ["EV_OPEN_JSON", ac_open_json, null],
2119
+ ["EV_EXPAND_PATH", ac_json_expand_path, null],
2120
+ ["EV_JSON_CLOSED", ac_json_closed, null],
1526
2121
  ["EV_SHOW_HOOK_DATA", ac_show_hook_data, null],
1527
2122
  ["EV_SHOW_TREEDB_TOPIC", ac_show_treedb_topic, null],
1528
2123
  ["EV_VERTEX_CLICKED", ac_vertex_clicked, null],
@@ -1536,9 +2131,10 @@ function create_gclass(gclass_name)
1536
2131
  ["EV_CLOSE_WINDOW", ac_close_window, null],
1537
2132
  ["EV_SET_LAYOUT", ac_set_layout, null],
1538
2133
  ["EV_SET_OPERATION_MODE", ac_set_operation_mode, null],
2134
+ ["EV_SET_FOCUS_TOPIC", ac_set_focus_topic, null],
1539
2135
  ["EV_SHOW", ac_show, null],
1540
2136
  ["EV_HIDE", ac_hide, null],
1541
- ["EV_RESIZE", ac_resize, null],
2137
+ ["EV_TRANSPORT_STATE", ac_transport_state, null],
1542
2138
  ]]
1543
2139
  ];
1544
2140
 
@@ -1550,7 +2146,12 @@ function create_gclass(gclass_name)
1550
2146
  ["EV_TREEDB_NODE_CREATED", event_flag_t.EVF_PUBLIC_EVENT],
1551
2147
  ["EV_TREEDB_NODE_UPDATED", event_flag_t.EVF_PUBLIC_EVENT],
1552
2148
  ["EV_TREEDB_NODE_DELETED", event_flag_t.EVF_PUBLIC_EVENT],
2149
+ ["EV_TREEDB_NODE_LINKED", event_flag_t.EVF_PUBLIC_EVENT],
2150
+ ["EV_TREEDB_NODE_UNLINKED", event_flag_t.EVF_PUBLIC_EVENT],
1553
2151
  ["EV_REFRESH_TREEDB", 0],
2152
+ ["EV_OPEN_JSON", 0],
2153
+ ["EV_EXPAND_PATH", 0],
2154
+ ["EV_JSON_CLOSED", 0],
1554
2155
  ["EV_SHOW_HOOK_DATA", 0],
1555
2156
  ["EV_SHOW_TREEDB_TOPIC", 0],
1556
2157
  ["EV_VERTEX_CLICKED", 0],
@@ -1564,11 +2165,12 @@ function create_gclass(gclass_name)
1564
2165
  ["EV_CLOSE_WINDOW", 0],
1565
2166
  ["EV_SET_LAYOUT", 0],
1566
2167
  ["EV_SET_OPERATION_MODE", 0],
2168
+ ["EV_SET_FOCUS_TOPIC", 0],
1567
2169
  ["EV_OPERATION_MODE_CHANGED",
1568
2170
  event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
1569
2171
  ["EV_SHOW", 0],
1570
2172
  ["EV_HIDE", 0],
1571
- ["EV_RESIZE", 0],
2173
+ ["EV_TRANSPORT_STATE", 0],
1572
2174
  ];
1573
2175
 
1574
2176
  /*----------------------------------------*