@yuneta/gobj-ui 3.0.0 → 4.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 +324 -6
  2. package/dist/gobj-ui.cjs.js +10891 -5006
  3. package/dist/gobj-ui.es.js +10915 -5064
  4. package/index.js +45 -1
  5. package/package.json +4 -4
  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 +18 -36
  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
@@ -33,16 +33,31 @@ import {
33
33
  gobj_read_str_attr,
34
34
  gobj_read_bool_attr,
35
35
  gobj_start,
36
+ gobj_stop,
37
+ gobj_is_running,
38
+ gobj_current_state,
36
39
  gobj_create_service,
40
+ gobj_create_pure_child,
37
41
  gobj_command,
38
42
  gobj_match_children,
39
43
  msg_iev_get_stack,
40
44
  kw_get_dict, gobj_stop_children,
41
45
  refresh_language,
46
+ gobj_destroy,
47
+ is_gobj,
48
+ gobj_is_destroying,
49
+ log_warning,
50
+ gclass_find_by_name,
51
+ clean_name,
52
+ is_object,
53
+ is_array,
54
+ empty_string,
42
55
  } from "@yuneta/gobj-js";
43
56
 
44
- import {yui_shell_show_error} from "./shell_modals.js";
45
- import {yui_shell_of} from "./c_yui_shell.js";
57
+ import "./c_yui_treedb_topics.css";
58
+
59
+ import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
60
+ import {yui_shell_of, yui_shell_set_sub_routes} from "./c_yui_shell.js";
46
61
 
47
62
  import {t} from "i18next";
48
63
 
@@ -61,6 +76,10 @@ SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB s
61
76
  SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
62
77
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
63
78
  SDATA(data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
79
+ SDATA(data_type_t.DTP_BOOLEAN, "with_cards_landing",0, false, "Land on a grid of topic cards (list->detail): a card opens its table, with the tabs bar + a back-to-grid button. Off = tabs only (legacy)."),
80
+ SDATA(data_type_t.DTP_JSON, "card_action_routes",0, null, "Per-card hash-route templates {info, table, graph} with a {topic} placeholder (host-supplied, route-agnostic). Present ⇒ cards show 3 icon actions; absent ⇒ a single card that opens the table."),
81
+ SDATA(data_type_t.DTP_JSON, "landing_routes", 0, null, "Host-supplied hashes for the two landing sub-views {cards, schema}; the toggle navigates to them so the landing is URL-addressable (ROUTING.md). Absent ⇒ toggle flips in-view only (legacy)."),
82
+ SDATA(data_type_t.DTP_STRING, "base_route", 0, "", "This view's base route (host-supplied); used to declare its sub-routes (topics / info / schema) to the site map (ROUTING.md contributor)."),
64
83
  SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Root HTML element, show/hide managed by external routing"),
65
84
  SDATA(data_type_t.DTP_POINTER, "$current_item", 0, null, "Currently selected item"),
66
85
  SDATA(data_type_t.DTP_STRING, "last_selection", 0, null, "Last href selection"),
@@ -73,6 +92,13 @@ let PRIVATE_DATA = {
73
92
  gobj_remote_yuno: null,
74
93
  descs: null,
75
94
  _topics_subscribed: {},
95
+ _pending_info: null, /* topic whose info panel to show once descs load */
96
+ _selected_card_topic: null, /* highlighted topic card in the landing grid */
97
+ _landing_view: "cards",/* landing sub-view: "cards" | "schema" */
98
+ schema_gobj: null, /* C_YUI_TREEDB_SCHEMA child (built lazily) */
99
+ json_gobj: null, /* C_YUI_JSON raw-tranger viewer (or null) */
100
+ json_win: null, /* C_YUI_WINDOW hosting it, desktop (or null) */
101
+ json_modal: null, /* shell modal hosting it, mobile (or null) */
76
102
  };
77
103
 
78
104
  let __gclass__ = null;
@@ -117,6 +143,13 @@ function mt_start(gobj)
117
143
  ***************************************************************/
118
144
  function mt_stop(gobj)
119
145
  {
146
+ /* Retire our site-map sub-routes so a torn-down view leaves no
147
+ * stale children in the map (ROUTING.md contributor). */
148
+ let shell = yui_shell_of(gobj);
149
+ if(shell) {
150
+ yui_shell_set_sub_routes(shell, gobj_read_str_attr(gobj, "base_route"), null);
151
+ }
152
+ close_json_viewer(gobj);
120
153
  gobj_stop_children(gobj);
121
154
  }
122
155
 
@@ -217,15 +250,125 @@ function build_ui(gobj)
217
250
  let $container = createElement2(
218
251
  ['div', {class: `C_YUI_TREEDB_TOPICS ${gobj_read_attr(gobj, "treedb_name")}`, style: 'height:100%; display:flex; flex-direction:column;'}, [
219
252
  ['div', {class: 'is-flex-grow-0'}, [
253
+ ['div', {class: 'is-flex is-align-items-center TREEDB_TOPICS_TOOLBAR',
254
+ style: 'gap:.25rem; padding:.25rem .25rem;'}, [
255
+ /* This toolbar never holds more than TWO buttons at once
256
+ * (back|toggle on the left, json on the right), so the
257
+ * labels stay on mobile — a deliberate exception to the
258
+ * icon-only-on-mobile rule: two bare squares side by side
259
+ * read as the same control. The graph's toolbar has many
260
+ * more, so it keeps is-hidden-mobile. */
261
+
262
+ /* Back to the topic-cards grid (cards-landing mode only);
263
+ * hidden until a topic is open. */
264
+ ['button', {class: 'button TREEDB_TOPICS_BACK is-hidden',
265
+ title: t('topics'), 'aria-label': t('topics'),
266
+ 'data-i18n-title': 'topics', 'data-i18n-aria-label': 'topics'}, [
267
+ ['span', {class: 'icon'}, [['i', {class: 'yi-arrow-left'}]]],
268
+ ['span', {i18n: 'topics'}, 'topics']
269
+ ], {
270
+ click: (evt) => {
271
+ evt.stopPropagation();
272
+ gobj_send_event(gobj, "EV_BACK_TO_TOPICS", {}, gobj);
273
+ }
274
+ }],
275
+ /* Landing view toggle: cards grid <-> schema graph
276
+ * (topics as nodes). Shown only on the landing. */
277
+ ['button', {class: 'button TREEDB_LANDING_TOGGLE is-hidden',
278
+ title: t('schema graph'), 'aria-label': t('schema graph'),
279
+ 'data-i18n-title': 'schema graph',
280
+ 'data-i18n-aria-label': 'schema graph'}, [
281
+ ['span', {class: 'icon'}, [['i', {class: 'yi-hexagon-nodes'}]]],
282
+ ['span', {i18n: 'schema'}, 'schema']
283
+ ], {
284
+ click: (evt) => {
285
+ evt.stopPropagation();
286
+ gobj_send_event(gobj, "EV_TOGGLE_LANDING_VIEW", {}, gobj);
287
+ }
288
+ }],
289
+ /* Inspect the treedb's raw tranger json (whole service,
290
+ * print-tranger, lazy drill). Last + margin-left:auto:
291
+ * flush right, away from the back arrow it sat next to. */
292
+ ['button', {class: 'button TREEDB_JSON_BTN',
293
+ style: 'margin-left:auto;',
294
+ title: t('raw json'), 'aria-label': t('raw json'),
295
+ 'data-i18n-title': 'raw json', 'data-i18n-aria-label': 'raw json'}, [
296
+ ['span', {class: 'icon'}, [['i', {class: 'yi-eye'}]]],
297
+ ['span', {i18n: 'raw json'}, 'raw json']
298
+ ], {
299
+ click: (evt) => {
300
+ evt.stopPropagation();
301
+ gobj_send_event(gobj, "EV_OPEN_JSON", {}, gobj);
302
+ }
303
+ }],
304
+ ]],
220
305
  ['div', {class: `tabs ${gobj_read_attr(gobj, "tabs_style")}`, style: ''}, [
221
306
  ['ul', {}]
222
307
  ]],
223
308
  ]],
224
309
  ['div', {class: 'is-flex-grow-1 sub-container', style: 'height:100%; min-height:0; overflow: auto;'}, [
225
- ]]
310
+ ]],
311
+ /* Cards-landing grid (list->detail): one card per topic, shown on
312
+ * entry; a click opens the topic's table. Empty/hidden in the
313
+ * legacy tabs-only mode. Reuses the shell's .yui-nav-cards look. */
314
+ ['div', {class: 'is-flex-grow-1 TREEDB_TOPICS_LANDING is-hidden',
315
+ style: 'min-height:0; overflow:auto;'}, [
316
+ ['div', {class: 'yui-nav-cards'}, []]
317
+ ]],
318
+ /* Schema-graph landing (topics as nodes): hosts a C_YUI_TREEDB_SCHEMA
319
+ * child, built lazily on first toggle. */
320
+ ['div', {class: 'is-flex-grow-1 TREEDB_TOPICS_SCHEMA is-hidden',
321
+ style: 'min-height:0; overflow:hidden;'}, []],
322
+ /* Routed topic-info panel (the card's info icon / .../<topic>/info):
323
+ * a read-only schema view built from the topic's desc. Hidden until
324
+ * the info route is active. */
325
+ ['div', {class: 'is-flex-grow-1 TREEDB_TOPIC_INFO is-hidden',
326
+ style: 'min-height:0; overflow:auto;'}, []]
226
327
  ]]
227
328
  );
228
329
  gobj_write_attr(gobj, "$container", $container);
330
+ refresh_language($container, t);
331
+ refresh_toolbar_buttons(gobj); /* starts disabled until connected / a tree topic */
332
+
333
+ /* Cards-landing: present the (still empty) grid from the first paint so
334
+ * there is no flash of an empty tabs bar while the schema loads. */
335
+ if(gobj_read_bool_attr(gobj, "with_cards_landing")) {
336
+ show_topics_landing(gobj);
337
+ }
338
+ }
339
+
340
+ /************************************************************
341
+ * Is the backend session live? The Raw JSON viewer issues a remote
342
+ * print-tranger, so it only makes sense with a session — the remote
343
+ * (C_IEVENT_CLI) is in ST_SESSION exactly while connected.
344
+ ************************************************************/
345
+ function is_connected(gobj)
346
+ {
347
+ let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
348
+ return !!remote && gobj_current_state(remote) === "ST_SESSION";
349
+ }
350
+
351
+ /************************************************************
352
+ * Enable the "Raw JSON" button only when the backend session is up (it
353
+ * issues a remote print-tranger). Called at build, on every tab change, and
354
+ * when the host forwards a transport edge (EV_TRANSPORT_STATE). `connected`
355
+ * defaults to the live session state; the host passes it explicitly on a
356
+ * transport edge because the remote's state may not have settled yet. Native
357
+ * `disabled` dims the button (Bulma) and blocks its click.
358
+ ************************************************************/
359
+ function refresh_toolbar_buttons(gobj, connected)
360
+ {
361
+ let $container = gobj_read_attr(gobj, "$container");
362
+ if(!$container) {
363
+ return;
364
+ }
365
+ if(typeof connected !== "boolean") {
366
+ connected = is_connected(gobj);
367
+ }
368
+ let $raw = $container.querySelector(".TREEDB_JSON_BTN");
369
+ if($raw) {
370
+ $raw.disabled = !connected;
371
+ }
229
372
  }
230
373
 
231
374
  /************************************************************
@@ -239,10 +382,10 @@ function show_load_error(gobj, message)
239
382
  if(!$container) {
240
383
  return;
241
384
  }
242
- let $err = $container.querySelector(".treedb-load-error");
385
+ let $err = $container.querySelector(".TOPICS_LOAD_ERROR");
243
386
  if(!$err) {
244
387
  $err = createElement2(
245
- ["div", {class: "notification is-danger is-light m-3 treedb-load-error"}, []]
388
+ ["div", {class: "TOPICS_LOAD_ERROR notification is-danger is-light m-3"}, []]
246
389
  );
247
390
  $container.insertBefore($err, $container.firstChild);
248
391
  }
@@ -330,13 +473,7 @@ function add_tab(gobj, gobj2, id, text, icon)
330
473
  */
331
474
  $a.addEventListener("click", function(ev) {
332
475
  ev.preventDefault();
333
- /* Switch locally now (instant, works standalone) … */
334
- gobj_send_event(gobj, "EV_SHOW", {href: id}, gobj);
335
- /* … and tell any host so it can mirror the topic into the
336
- * URL path (deep-link / reload restore). Optional sub:
337
- * EVF_NO_WARN_SUBS so standalone use stays quiet. */
338
- let topic = id.indexOf("?") >= 0 ? id.split("?")[1] : id;
339
- gobj_publish_event(gobj, "EV_TOPIC_SELECTED", {topic: topic});
476
+ select_topic_by_id(gobj, id);
340
477
  });
341
478
 
342
479
  /*
@@ -369,6 +506,452 @@ function remove_tab(gobj, gobj2, id)
369
506
  if($child_content && $child_content.parentNode) {
370
507
  $child_content.parentNode.removeChild($child_content);
371
508
  }
509
+ /* Drop its landing card too, if any. */
510
+ let $card = $container.querySelector(
511
+ `.TREEDB_TOPIC_CARD[data-topic-href="${id}"]`);
512
+ if($card && $card.parentNode) {
513
+ $card.parentNode.removeChild($card);
514
+ }
515
+ }
516
+
517
+ /************************************************************
518
+ * Select a topic: the single entry point for a tab click AND a
519
+ * landing-card click. Switch locally now (instant, works standalone)
520
+ * and tell any host so it can mirror the topic into the URL path
521
+ * (deep-link / reload restore). Optional sub: EVF_NO_WARN_SUBS so
522
+ * standalone use stays quiet.
523
+ ************************************************************/
524
+ function select_topic_by_id(gobj, id)
525
+ {
526
+ gobj_send_event(gobj, "EV_SHOW", {href: id}, gobj);
527
+ let topic = id.indexOf("?") >= 0 ? id.split("?")[1] : id;
528
+ gobj_publish_event(gobj, "EV_TOPIC_SELECTED", {topic: topic});
529
+ }
530
+
531
+ /************************************************************
532
+ * One icon-action anchor of a topic card: a REAL hash link
533
+ * (deep-linkable, middle-clickable) — the shell routes it on
534
+ * hashchange, no JS click handler needed.
535
+ ************************************************************/
536
+ function card_action_anchor(logical_class, icon, i18n_key, href)
537
+ {
538
+ return ['a', {
539
+ class: logical_class,
540
+ href: href,
541
+ title: t(i18n_key),
542
+ 'aria-label': t(i18n_key),
543
+ 'data-i18n-title': i18n_key,
544
+ 'data-i18n-aria-label': i18n_key
545
+ }, [
546
+ ['span', {class: 'icon'}, [['i', {class: icon, 'aria-hidden': 'true'}]]]
547
+ ]];
548
+ }
549
+
550
+ /************************************************************
551
+ * Add one topic card to the cards-landing grid (list->detail).
552
+ * With `card_action_routes` (host-supplied templates) the card
553
+ * shows three hash-routed icons — info / table / graph. Without,
554
+ * it falls back to a single card that opens the table (same id
555
+ * contract as the tab, `<gobj>?<topic>`).
556
+ ************************************************************/
557
+ function add_topic_card(gobj, id, text, icon)
558
+ {
559
+ let $container = gobj_read_attr(gobj, "$container");
560
+ let $grid = $container.querySelector(".TREEDB_TOPICS_LANDING .yui-nav-cards");
561
+ if(!$grid) {
562
+ return;
563
+ }
564
+
565
+ let routes = gobj_read_attr(gobj, "card_action_routes");
566
+ if(!is_object(routes)) {
567
+ /* Legacy single-action card: the whole card opens the table. */
568
+ let $card = createElement2(
569
+ ['a', {class: 'yui-nav-item yui-nav-card TREEDB_TOPIC_CARD',
570
+ href: '#', 'data-topic-href': id,
571
+ 'aria-label': text, 'data-i18n-aria-label': text}, [
572
+ ['span', {class: 'icon is-medium'}, [['i', {class: icon || 'yi-table',
573
+ 'aria-hidden': 'true'}]]],
574
+ ['span', {class: 'yui-nav-label', i18n: text}, text]
575
+ ]]
576
+ );
577
+ $card.addEventListener("click", function(ev) {
578
+ ev.preventDefault();
579
+ select_topic_by_id(gobj, id);
580
+ });
581
+ $grid.appendChild($card);
582
+ refresh_language($card, t);
583
+ return;
584
+ }
585
+
586
+ /* Three-icon card: info / table / graph, all real hash anchors.
587
+ * {topic} in each template is the raw topic name (identifier-safe,
588
+ * matching the tab/host `<base_route>/<topic>` convention). */
589
+ let topic = id.indexOf("?") >= 0 ? id.split("?")[1] : id;
590
+ let fill = (tpl) => String(tpl || "").replace("{topic}", topic);
591
+
592
+ let actions = [];
593
+ if(routes.info) {
594
+ actions.push(card_action_anchor(
595
+ "TREEDB_CARD_INFO", "yi-circle-info", "info", fill(routes.info)));
596
+ }
597
+ if(routes.table) {
598
+ actions.push(card_action_anchor(
599
+ "TREEDB_CARD_TABLE", "yi-table", "table", fill(routes.table)));
600
+ }
601
+ if(routes.graph) {
602
+ actions.push(card_action_anchor(
603
+ "TREEDB_CARD_GRAPH", "yi-hexagon-nodes", "graph", fill(routes.graph)));
604
+ }
605
+
606
+ let $card = createElement2(
607
+ ['div', {class: 'TREEDB_TOPIC_CARD', 'data-topic': topic}, [
608
+ ['span', {class: 'TREEDB_TOPIC_CARD_NAME', i18n: text}, text],
609
+ ['div', {class: 'TREEDB_TOPIC_CARD_ACTIONS'}, actions]
610
+ ]]
611
+ );
612
+ if(gobj.priv._selected_card_topic === topic) {
613
+ $card.classList.add("is-selected");
614
+ }
615
+ /* A click anywhere on the card selects it (highlight). A click that
616
+ * ALSO lands on an icon still runs the icon's hash navigation (we don't
617
+ * preventDefault) — "click outside = select, click an icon = select +
618
+ * enter". Selection crosses the FSM like every action. */
619
+ $card.addEventListener("click", function() {
620
+ gobj_send_event(gobj, "EV_SELECT_TOPIC_CARD", {topic: topic}, gobj);
621
+ });
622
+ $grid.appendChild($card);
623
+ refresh_language($card, t);
624
+ }
625
+
626
+ /************************************************************
627
+ * Cards-landing mode: show the topic-cards grid (the section
628
+ * index), hide the tabs bar + the topic table, hide the back
629
+ * button. Deactivate any active tab so nothing looks selected.
630
+ ************************************************************/
631
+ function show_topics_landing(gobj)
632
+ {
633
+ let $container = gobj_read_attr(gobj, "$container");
634
+ if(!$container) {
635
+ return;
636
+ }
637
+ let $tabs = $container.querySelector(".tabs");
638
+ let $sub = $container.querySelector(".sub-container");
639
+ let $landing = $container.querySelector(".TREEDB_TOPICS_LANDING");
640
+ let $schema = $container.querySelector(".TREEDB_TOPICS_SCHEMA");
641
+ let $info = $container.querySelector(".TREEDB_TOPIC_INFO");
642
+ let $back = $container.querySelector(".TREEDB_TOPICS_BACK");
643
+ let $toggle = $container.querySelector(".TREEDB_LANDING_TOGGLE");
644
+ if($tabs) {
645
+ $tabs.classList.add("is-hidden");
646
+ }
647
+ if($sub) {
648
+ $sub.classList.add("is-hidden");
649
+ }
650
+ if($info) {
651
+ $info.classList.add("is-hidden");
652
+ }
653
+ if($back) {
654
+ $back.classList.add("is-hidden");
655
+ }
656
+ /* Cards grid vs schema graph — the two landing sub-views. */
657
+ let schema_mode = gobj.priv._landing_view === "schema";
658
+ if($toggle) {
659
+ $toggle.classList.remove("is-hidden"); /* the toggle lives on the landing */
660
+ $toggle.classList.toggle("is-primary", schema_mode);
661
+ }
662
+ if(schema_mode) {
663
+ if($landing) {
664
+ $landing.classList.add("is-hidden");
665
+ }
666
+ /* Reveal the pane BEFORE building: G6 renders at 0×0 in a
667
+ * display:none container. */
668
+ if($schema) {
669
+ $schema.classList.remove("is-hidden");
670
+ }
671
+ build_schema_child(gobj);
672
+ if(gobj.priv.schema_gobj) {
673
+ gobj_send_event(gobj.priv.schema_gobj, "EV_SHOW", {}, gobj);
674
+ }
675
+ } else {
676
+ if($schema) {
677
+ $schema.classList.add("is-hidden");
678
+ }
679
+ if($landing) {
680
+ $landing.classList.remove("is-hidden");
681
+ }
682
+ }
683
+
684
+ /* Hide the currently shown topic content + deactivate its tab. */
685
+ let $current_item = gobj_read_attr(gobj, "$current_item");
686
+ if($current_item) {
687
+ $current_item.classList.remove("is-active");
688
+ let gobj2 = $current_item.gobj;
689
+ if(gobj2) {
690
+ let $child = gobj_read_attr(gobj2, "$container");
691
+ if($child) {
692
+ $child.classList.add("is-hidden");
693
+ }
694
+ }
695
+ }
696
+ }
697
+
698
+ /************************************************************
699
+ * Lazily build the schema-graph child (C_YUI_TREEDB_SCHEMA) into
700
+ * the landing's schema pane. It draws the treedb as topics+links
701
+ * from `descs` (already loaded) and navigates on a node click via
702
+ * the table hash route.
703
+ ************************************************************/
704
+ function build_schema_child(gobj)
705
+ {
706
+ let priv = gobj.priv;
707
+ if(priv.schema_gobj) {
708
+ return;
709
+ }
710
+ /* Wait for the schema: on a deep-link/F5 to `.../schema` the route
711
+ * arrives before `descs` load. Building now would render an empty
712
+ * graph that never refreshes; process_treedb_descs re-shows the
713
+ * landing once descs arrive, which calls us again. */
714
+ if(!is_object(gobj_read_attr(gobj, "descs"))) {
715
+ return;
716
+ }
717
+ if(gclass_find_by_name("C_YUI_TREEDB_SCHEMA") === null) {
718
+ log_error(`${gobj_short_name(gobj)}: C_YUI_TREEDB_SCHEMA not registered by the app`);
719
+ return;
720
+ }
721
+ let $container = gobj_read_attr(gobj, "$container");
722
+ let $pane = $container && $container.querySelector(".TREEDB_TOPICS_SCHEMA");
723
+ if(!$pane) {
724
+ return;
725
+ }
726
+ let routes = gobj_read_attr(gobj, "card_action_routes");
727
+ let node_route = (is_object(routes) && routes.table) ? routes.table : "";
728
+
729
+ let schema = gobj_create_pure_child(
730
+ "schema_" + clean_name(gobj_name(gobj)),
731
+ "C_YUI_TREEDB_SCHEMA",
732
+ {
733
+ subscriber: gobj,
734
+ descs: gobj_read_attr(gobj, "descs"),
735
+ node_route: node_route,
736
+ system: gobj_read_bool_attr(gobj, "system")
737
+ },
738
+ gobj
739
+ );
740
+ if(!schema) {
741
+ log_error(`${gobj_short_name(gobj)}: cannot create the schema view`);
742
+ return;
743
+ }
744
+ priv.schema_gobj = schema;
745
+ let $sc = gobj_read_attr(schema, "$container");
746
+ if($sc) {
747
+ $pane.appendChild($sc);
748
+ }
749
+ gobj_start(schema);
750
+ }
751
+
752
+ /************************************************************
753
+ * Cards-landing mode: enter a topic (hide the grid, show the
754
+ * tabs bar + the topic table + the back button). No-op unless
755
+ * the landing is enabled.
756
+ ************************************************************/
757
+ function show_topic_detail(gobj)
758
+ {
759
+ if(!gobj_read_bool_attr(gobj, "with_cards_landing")) {
760
+ return;
761
+ }
762
+ let $container = gobj_read_attr(gobj, "$container");
763
+ if(!$container) {
764
+ return;
765
+ }
766
+ let $tabs = $container.querySelector(".tabs");
767
+ let $sub = $container.querySelector(".sub-container");
768
+ let $landing = $container.querySelector(".TREEDB_TOPICS_LANDING");
769
+ let $schema = $container.querySelector(".TREEDB_TOPICS_SCHEMA");
770
+ let $info = $container.querySelector(".TREEDB_TOPIC_INFO");
771
+ let $back = $container.querySelector(".TREEDB_TOPICS_BACK");
772
+ let $toggle = $container.querySelector(".TREEDB_LANDING_TOGGLE");
773
+ if($landing) {
774
+ $landing.classList.add("is-hidden");
775
+ }
776
+ if($schema) {
777
+ $schema.classList.add("is-hidden");
778
+ }
779
+ if($info) {
780
+ $info.classList.add("is-hidden");
781
+ }
782
+ if($toggle) {
783
+ $toggle.classList.add("is-hidden"); /* landing-only */
784
+ }
785
+ if($tabs) {
786
+ $tabs.classList.remove("is-hidden");
787
+ }
788
+ if($sub) {
789
+ $sub.classList.remove("is-hidden");
790
+ }
791
+ if($back) {
792
+ $back.classList.remove("is-hidden");
793
+ }
794
+ }
795
+
796
+ /************************************************************
797
+ * Cards-landing mode: show the routed topic-info panel (the card's
798
+ * info icon / .../<topic>/info) — a read-only schema view built from
799
+ * the topic's desc. Hide the grid, tabs and table; show the back
800
+ * button. If the schema has not arrived yet, remember the topic and
801
+ * replay once descs load (deep-link / F5 on an info URL).
802
+ ************************************************************/
803
+ function show_topic_info(gobj, topic)
804
+ {
805
+ let priv = gobj.priv;
806
+ if(empty_string(topic)) {
807
+ show_topics_landing(gobj);
808
+ return;
809
+ }
810
+ let descs = gobj_read_attr(gobj, "descs");
811
+ if(!descs) {
812
+ priv._pending_info = topic; /* wait for the schema */
813
+ return;
814
+ }
815
+ let desc = descs[topic];
816
+ if(!is_object(desc)) {
817
+ /* Unknown topic (stale deep-link): fall back to the grid. */
818
+ log_error(`${gobj_short_name(gobj)}: info for unknown topic '${topic}'`);
819
+ show_topics_landing(gobj);
820
+ return;
821
+ }
822
+
823
+ let $container = gobj_read_attr(gobj, "$container");
824
+ let $info = $container && $container.querySelector(".TREEDB_TOPIC_INFO");
825
+ if(!$info) {
826
+ return;
827
+ }
828
+ build_topic_info_panel(gobj, $info, topic, desc);
829
+ refresh_language($info, t);
830
+
831
+ let $tabs = $container.querySelector(".tabs");
832
+ let $sub = $container.querySelector(".sub-container");
833
+ let $landing = $container.querySelector(".TREEDB_TOPICS_LANDING");
834
+ let $schema = $container.querySelector(".TREEDB_TOPICS_SCHEMA");
835
+ let $back = $container.querySelector(".TREEDB_TOPICS_BACK");
836
+ let $toggle = $container.querySelector(".TREEDB_LANDING_TOGGLE");
837
+ if($landing) {
838
+ $landing.classList.add("is-hidden");
839
+ }
840
+ if($schema) {
841
+ $schema.classList.add("is-hidden");
842
+ }
843
+ if($toggle) {
844
+ $toggle.classList.add("is-hidden");
845
+ }
846
+ if($tabs) {
847
+ $tabs.classList.add("is-hidden");
848
+ }
849
+ if($sub) {
850
+ $sub.classList.add("is-hidden");
851
+ }
852
+ if($info) {
853
+ $info.classList.remove("is-hidden");
854
+ }
855
+ if($back) {
856
+ $back.classList.remove("is-hidden");
857
+ }
858
+ }
859
+
860
+ /************************************************************
861
+ * Build the read-only topic-info panel from a topic desc: the
862
+ * topic name, its pkey, and a table of columns (name / type /
863
+ * key relationship). Everything is guarded — a malformed desc
864
+ * renders a shorter panel, never throws.
865
+ ************************************************************/
866
+ function build_topic_info_panel(gobj, $info, topic, desc)
867
+ {
868
+ while($info.firstChild) {
869
+ $info.removeChild($info.firstChild);
870
+ }
871
+
872
+ let pkey = desc.pkey || "";
873
+
874
+ /* Topic metadata (version matters to the operator): version /
875
+ * system / pkey / tkey. Each row is shown only when its value is
876
+ * present; the version is emphasised as a tag. */
877
+ let fmt_flag = (v) => {
878
+ if(is_array(v)) {
879
+ return v.join(", ");
880
+ }
881
+ if(is_object(v)) {
882
+ return Object.keys(v).join(", ");
883
+ }
884
+ return (v === undefined || v === null) ? "" : String(v);
885
+ };
886
+ let $meta = [];
887
+ let push_meta = (key, value, highlight) => {
888
+ if(value === undefined || value === null || value === "") {
889
+ return;
890
+ }
891
+ let $val = highlight
892
+ ? ["span", {class: "tag is-info"}, `${value}`]
893
+ : ["code", {}, `${value}`];
894
+ $meta.push(
895
+ ["tr", {}, [
896
+ ["th", {i18n: key, style: "width:11rem;"}, key],
897
+ ["td", {}, [$val]]
898
+ ]]
899
+ );
900
+ };
901
+ push_meta("version", desc.topic_version, true);
902
+ push_meta("system", fmt_flag(desc.system_flag));
903
+ push_meta("pkey", pkey);
904
+ push_meta("tkey", desc.tkey);
905
+
906
+ let $rows = [];
907
+ let cols = is_array(desc.cols) ? desc.cols : [];
908
+ for(let col of cols) {
909
+ if(!col || !col.id || col.id.charAt(0) === "_") {
910
+ continue;
911
+ }
912
+ let type = col.type || (is_array(col.flag) ? col.flag.join(", ") : (col.flag || ""));
913
+ let rel = "";
914
+ if(col.id === pkey) {
915
+ rel = "pkey";
916
+ } else if(is_object(col.fkey)) {
917
+ rel = "→ " + Object.keys(col.fkey).join(", ");
918
+ } else if(is_object(col.hook)) {
919
+ rel = "hook → " + Object.keys(col.hook).join(", ");
920
+ }
921
+ $rows.push(
922
+ ["tr", {}, [
923
+ ["td", {}, [["code", {}, `${col.id}`]]],
924
+ ["td", {}, `${type}`],
925
+ ["td", {}, `${rel}`]
926
+ ]]
927
+ );
928
+ }
929
+
930
+ let $panel = createElement2(
931
+ ["div", {class: "TREEDB_TOPIC_INFO_CARD content"}, [
932
+ ["h3", {class: "TREEDB_TOPIC_INFO_TITLE title is-5"}, [
933
+ ["span", {class: "icon-text"}, [
934
+ ["span", {class: "icon"}, [["i", {class: "yi-circle-info"}]]],
935
+ ["span", {i18n: `${topic}`}, `${topic}`]
936
+ ]]
937
+ ]],
938
+ ["table", {class: "table is-narrow TREEDB_TOPIC_INFO_META mb-4"}, [
939
+ ["tbody", {}, $meta]
940
+ ]],
941
+ ["h4", {class: "title is-6 mb-2", i18n: "columns"}, "columns"],
942
+ ["table", {class: "table is-fullwidth is-striped is-narrow"}, [
943
+ ["thead", {}, [
944
+ ["tr", {}, [
945
+ ["th", {i18n: "column"}, "column"],
946
+ ["th", {i18n: "type"}, "type"],
947
+ ["th", {i18n: "key"}, "key"]
948
+ ]]
949
+ ]],
950
+ ["tbody", {}, $rows]
951
+ ]]
952
+ ]]
953
+ );
954
+ $info.appendChild($panel);
372
955
  }
373
956
 
374
957
  /************************************************************
@@ -445,6 +1028,7 @@ function process_treedb_descs(gobj)
445
1028
 
446
1029
  // TODO get icon from remote config
447
1030
  add_tab(gobj, gobj_topic_form, id, key, "yi-table");
1031
+ add_topic_card(gobj, id, key, "yi-table");
448
1032
 
449
1033
  gobj_start(gobj_topic_form);
450
1034
  }
@@ -457,26 +1041,42 @@ function process_treedb_descs(gobj)
457
1041
  * (e.g. the persisted topic no longer exists in the schema).
458
1042
  */
459
1043
  let href = gobj_read_str_attr(gobj, "last_selection");
460
- if(!href) {
461
- try {
462
- let topic = window.localStorage.getItem(
463
- `yui_treedb_topics:${gobj_name(gobj)}`
464
- );
465
- if(topic) {
466
- href = `${gobj_name(gobj)}?${topic}`;
1044
+ if(gobj_read_bool_attr(gobj, "with_cards_landing")) {
1045
+ /* Cards-landing: the grid IS the entry point, so DON'T auto-restore
1046
+ * the persisted topic only honour an explicit deep-link (a host
1047
+ * EV_SHOW / EV_SHOW_TOPIC_INFO carrying a topic arrived while descs
1048
+ * were loading). No topic in flight ⇒ land on the grid. */
1049
+ if(gobj.priv._pending_info) {
1050
+ let ti = gobj.priv._pending_info;
1051
+ gobj.priv._pending_info = null;
1052
+ show_topic_info(gobj, ti);
1053
+ } else if(href && href.indexOf("?") >= 0) {
1054
+ gobj_send_event(gobj, "EV_SHOW", {href: href}, gobj);
1055
+ } else {
1056
+ show_topics_landing(gobj);
1057
+ }
1058
+ } else {
1059
+ if(!href) {
1060
+ try {
1061
+ let topic = window.localStorage.getItem(
1062
+ `yui_treedb_topics:${gobj_name(gobj)}`
1063
+ );
1064
+ if(topic) {
1065
+ href = `${gobj_name(gobj)}?${topic}`;
1066
+ }
1067
+ } catch(e) {
1068
+ // localStorage unavailable — fall through to first tab
467
1069
  }
468
- } catch(e) {
469
- // localStorage unavailable — fall through to first tab
470
1070
  }
1071
+ gobj_send_event(
1072
+ gobj,
1073
+ "EV_SHOW",
1074
+ {
1075
+ href: href
1076
+ },
1077
+ gobj
1078
+ );
471
1079
  }
472
- gobj_send_event(
473
- gobj,
474
- "EV_SHOW",
475
- {
476
- href: href
477
- },
478
- gobj
479
- );
480
1080
 
481
1081
  /*
482
1082
  * Subscribe events to manage Ui_treedb_topic_form kids
@@ -493,6 +1093,49 @@ function process_treedb_descs(gobj)
493
1093
  let topic_name = gobj_read_attr(kid, "topic_name");
494
1094
  get_nodes(gobj, topic_name);
495
1095
  }
1096
+
1097
+ /* Declare our view-owned sub-routes to the site map now the schema
1098
+ * is known (ROUTING.md contributor). */
1099
+ register_sub_routes(gobj);
1100
+ }
1101
+
1102
+ /************************************************************
1103
+ * Declare this view's deep, view-owned sub-routes to the site map:
1104
+ * the schema landing, and per-topic table + info. Route-agnostic
1105
+ * except for the host-supplied `base_route`. Cleared on stop.
1106
+ ************************************************************/
1107
+ function register_sub_routes(gobj)
1108
+ {
1109
+ let shell = yui_shell_of(gobj);
1110
+ let base = gobj_read_str_attr(gobj, "base_route");
1111
+ if(!shell || empty_string(base)) {
1112
+ return;
1113
+ }
1114
+ let system = gobj_read_bool_attr(gobj, "system");
1115
+ let descs = gobj_read_attr(gobj, "descs");
1116
+ let nodes = [];
1117
+ if(is_object(gobj_read_attr(gobj, "landing_routes"))) {
1118
+ nodes.push({route: base + "/schema", label: "schema",
1119
+ icon: "yi-hexagon-nodes", gclass: "C_YUI_TREEDB_SCHEMA"});
1120
+ }
1121
+ if(is_object(descs)) {
1122
+ for(let topic of Object.keys(descs)) {
1123
+ if(!system && topic.substring(0, 2) === "__") {
1124
+ continue;
1125
+ }
1126
+ nodes.push({
1127
+ route: base + "/" + topic,
1128
+ label: topic,
1129
+ icon: "yi-table",
1130
+ gclass: GCLASS_NAME,
1131
+ children: [
1132
+ {route: base + "/" + topic + "/info", label: "info",
1133
+ icon: "yi-circle-info", gclass: GCLASS_NAME}
1134
+ ]
1135
+ });
1136
+ }
1137
+ }
1138
+ yui_shell_set_sub_routes(shell, base, nodes);
496
1139
  }
497
1140
 
498
1141
  /************************************************************
@@ -776,6 +1419,203 @@ function treedb_delete_node(gobj, treedb_name, topic_name, record, options)
776
1419
  }
777
1420
  }
778
1421
 
1422
+ /************************************************************
1423
+ * True on a phone-width viewport (Bulma's mobile breakpoint).
1424
+ ************************************************************/
1425
+ function is_mobile()
1426
+ {
1427
+ return typeof window !== "undefined" && window.innerWidth <= 768;
1428
+ }
1429
+
1430
+ /************************************************************
1431
+ * JSON viewer (a single C_YUI_JSON in a window/modal): print-tranger of the
1432
+ * whole service (lazy drill). CHILD model: the viewer publishes
1433
+ * EV_EXPAND_PATH to us.
1434
+ ************************************************************/
1435
+ function open_json_viewer(gobj)
1436
+ {
1437
+ let priv = gobj.priv;
1438
+
1439
+ /* Already open: just re-fetch. */
1440
+ if(priv.json_win || priv.json_modal) {
1441
+ request_print_tranger(gobj, "");
1442
+ return;
1443
+ }
1444
+
1445
+ if(gclass_find_by_name("C_YUI_JSON") === null) {
1446
+ log_error(`${gobj_short_name(gobj)}: C_YUI_JSON not registered by the app`);
1447
+ yui_shell_show_error(yui_shell_of(gobj), "raw json viewer unavailable", {t: t});
1448
+ return;
1449
+ }
1450
+
1451
+ let mobile = is_mobile();
1452
+ let shell = yui_shell_of(gobj);
1453
+
1454
+ let jv = gobj_create_service(
1455
+ `treedb-topics-json-${clean_name(gobj_name(gobj))}`,
1456
+ "C_YUI_JSON",
1457
+ {
1458
+ /* No `title`: the host titles it — the window's title bar on
1459
+ * desktop, the dialog's header on mobile. The viewer's own
1460
+ * title would land INSIDE that host, doubling it. */
1461
+ subscriber: gobj /* publishes EV_EXPAND_PATH to us */
1462
+ },
1463
+ gobj
1464
+ );
1465
+ if(!jv) {
1466
+ log_error(`${gobj_short_name(gobj)}: cannot create the JSON viewer`);
1467
+ return;
1468
+ }
1469
+ priv.json_gobj = jv;
1470
+ gobj_start(jv);
1471
+ let $box = gobj_read_pointer_attr(jv, "$container");
1472
+
1473
+ if(mobile) {
1474
+ if(!shell) {
1475
+ log_error(`${gobj_short_name(gobj)}: no shell, cannot open the JSON sheet`);
1476
+ close_json_viewer(gobj);
1477
+ return;
1478
+ }
1479
+ priv.json_modal = yui_shell_show_modal(shell, $box, {
1480
+ dialog: true,
1481
+ logical_class: "TREEDB_JSON_SHEET",
1482
+ title_prefix: priv.treedb_name,
1483
+ title: "raw json",
1484
+ t: t,
1485
+ on_close: () => {
1486
+ if(gobj_is_destroying(gobj)) {
1487
+ return;
1488
+ }
1489
+ gobj_send_event(gobj, "EV_JSON_CLOSED", {}, gobj);
1490
+ }
1491
+ });
1492
+ } else {
1493
+ let $win_parent = (shell && yui_shell_popup_layer(shell)) ||
1494
+ (typeof document !== "undefined" && document.getElementById("top-layer")) ||
1495
+ null;
1496
+
1497
+ priv.json_win = gobj_create_service(
1498
+ `treedb-topics-jsonwin-${clean_name(gobj_name(gobj))}`,
1499
+ "C_YUI_WINDOW",
1500
+ {
1501
+ $parent: $win_parent,
1502
+ subscriber: null,
1503
+ modal: false,
1504
+ showMax: true,
1505
+ showFooter: false,
1506
+ resizable: true,
1507
+ center: true,
1508
+ auto_save_size_and_position: true,
1509
+ width: 640,
1510
+ height: 620,
1511
+ logical_class: "TREEDB_JSON_WINDOW",
1512
+ title_prefix: priv.treedb_name,
1513
+ title: "raw json",
1514
+ icon: "yi-eye",
1515
+ body: $box,
1516
+ manager: null,
1517
+ on_close: () => {
1518
+ if(gobj_is_destroying(gobj)) {
1519
+ return;
1520
+ }
1521
+ gobj_send_event(gobj, "EV_JSON_CLOSED", {}, gobj);
1522
+ }
1523
+ },
1524
+ gobj
1525
+ );
1526
+ if(!priv.json_win) {
1527
+ log_error(`${gobj_short_name(gobj)}: cannot create the JSON window`);
1528
+ close_json_viewer(gobj);
1529
+ return;
1530
+ }
1531
+ gobj_start(priv.json_win);
1532
+ }
1533
+
1534
+ request_print_tranger(gobj, "");
1535
+ }
1536
+
1537
+ /************************************************************
1538
+ * Close the JSON viewer (user dismiss / teardown).
1539
+ ************************************************************/
1540
+ function close_json_viewer(gobj)
1541
+ {
1542
+ let priv = gobj.priv;
1543
+ let jv = priv.json_gobj;
1544
+ let win = priv.json_win;
1545
+ let modal = priv.json_modal;
1546
+
1547
+ priv.json_gobj = null;
1548
+ priv.json_win = null;
1549
+ priv.json_modal = null;
1550
+
1551
+ if(win && is_gobj(win)) {
1552
+ try {
1553
+ /* STOP, then destroy — same rule as the viewer below: the
1554
+ * window was STARTED on open, and gobj_destroy() raises the
1555
+ * `destroying` flag before it can stop a running gobj, so
1556
+ * destroying it straight logs two errors and skips mt_stop.
1557
+ * The ✕ path already stopped it (close_window) — guard. */
1558
+ if(gobj_is_running(win)) {
1559
+ gobj_stop(win);
1560
+ }
1561
+ gobj_destroy(win);
1562
+ } catch(e) {
1563
+ log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);
1564
+ }
1565
+ }
1566
+ if(modal && typeof modal.close === "function") {
1567
+ try {
1568
+ modal.close();
1569
+ } catch(e) {
1570
+ log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);
1571
+ }
1572
+ }
1573
+ if(jv && is_gobj(jv)) {
1574
+ try {
1575
+ /* STOP, then destroy — the viewer was STARTED in open_json_viewer.
1576
+ * gobj_destroy() raises the `destroying` flag before it can stop a
1577
+ * running gobj, so destroying it straight logs "Destroying a
1578
+ * RUNNING gobj" + "gobj NULL or DESTROYED" and skips its mt_stop. */
1579
+ gobj_stop(jv);
1580
+ gobj_destroy(jv);
1581
+ } catch(e) {
1582
+ log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);
1583
+ }
1584
+ }
1585
+ }
1586
+
1587
+ /************************************************************
1588
+ * Fetch the treedb's raw tranger (or one subtree when `path` is set),
1589
+ * collapsed at 100 so a huge tranger stays a small payload of
1590
+ * `__collapsed__` stubs the viewer expands on demand.
1591
+ ************************************************************/
1592
+ function request_print_tranger(gobj, path)
1593
+ {
1594
+ let priv = gobj.priv;
1595
+ let remote = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
1596
+ if(!remote) {
1597
+ log_error(`${gobj_short_name(gobj)}: No gobj_remote_yuno defined`);
1598
+ let jv = priv.json_gobj;
1599
+ if(path && jv && is_gobj(jv) && !gobj_is_destroying(jv)) {
1600
+ gobj_send_event(jv, "EV_SUBTREE_ERROR",
1601
+ {path: path, error: t("no session")}, gobj);
1602
+ }
1603
+ return;
1604
+ }
1605
+ let ret = gobj_command(remote, "print-tranger",
1606
+ {
1607
+ service: priv.treedb_name,
1608
+ expanded: 1,
1609
+ lists_limit: 100,
1610
+ dicts_limit: 100,
1611
+ path: path || ""
1612
+ }, gobj);
1613
+ if(ret) {
1614
+ log_error(ret);
1615
+ }
1616
+ }
1617
+
1618
+
779
1619
 
780
1620
 
781
1621
 
@@ -811,6 +1651,35 @@ function ac_mt_command_answer(gobj, event, kw, src)
811
1651
  let command = kw_get_str(gobj, __command__, "command", "", kw_flag_t.KW_REQUIRED);
812
1652
  let kw_command = kw_get_dict(gobj, __command__, "kw", {}, kw_flag_t.KW_REQUIRED);
813
1653
 
1654
+ /*
1655
+ * The JSON viewer's feed: `print-tranger` (whole tranger, lazy drill by
1656
+ * echoed `path`). Handled before the generic error path, which returns
1657
+ * early.
1658
+ */
1659
+ if(command === "print-tranger") {
1660
+ let jv = gobj.priv.json_gobj;
1661
+ if(!jv || !is_gobj(jv) || gobj_is_destroying(jv)) {
1662
+ return 0; /* viewer closed before its answer landed: benign */
1663
+ }
1664
+ let path = kw_get_str(gobj, kw_command, "path", "", 0);
1665
+ if(result < 0) {
1666
+ if(path) {
1667
+ gobj_send_event(jv, "EV_SUBTREE_ERROR",
1668
+ {path: path, error: comment || "print-tranger failed"}, gobj);
1669
+ } else {
1670
+ yui_shell_show_error(yui_shell_of(gobj),
1671
+ comment || "print-tranger failed", {t: t});
1672
+ }
1673
+ return 0;
1674
+ }
1675
+ if(path) {
1676
+ gobj_send_event(jv, "EV_SUBTREE_LOADED", {path: path, json: data}, gobj);
1677
+ } else {
1678
+ gobj_send_event(jv, "EV_SET_JSON", {json: data}, gobj);
1679
+ }
1680
+ return 0;
1681
+ }
1682
+
814
1683
  if(result < 0) {
815
1684
  if(command === "descs") {
816
1685
  /* The schema couldn't load (not a treedb, no authz for it, backend
@@ -873,6 +1742,19 @@ function ac_show(gobj, event, kw, src)
873
1742
  {
874
1743
  let href = kw.href;
875
1744
 
1745
+ /* Cards-landing: a show without a concrete `?<topic>` means "enter the
1746
+ * workspace" — land on the grid instead of auto-opening the first tab.
1747
+ * The bare tab route is always the CARDS sub-view (schema has its own
1748
+ * `.../schema` route), so reset it here — this is what makes Back from
1749
+ * the schema landing return to the cards. */
1750
+ if(gobj_read_bool_attr(gobj, "with_cards_landing") &&
1751
+ (!href || href.indexOf("?") < 0)) {
1752
+ gobj.priv._landing_view = "cards";
1753
+ show_topics_landing(gobj);
1754
+ refresh_toolbar_buttons(gobj);
1755
+ return 0;
1756
+ }
1757
+
876
1758
  let $container = gobj_read_attr(gobj, "$container");
877
1759
  let $current_item = gobj_read_attr(gobj, "$current_item");
878
1760
  let $a = $container.querySelector(`a[href="${href}"]`);
@@ -943,8 +1825,107 @@ function ac_show(gobj, event, kw, src)
943
1825
  }
944
1826
  gobj_send_event(gobj2, "EV_SHOW", kw, gobj);
945
1827
  }
1828
+ /* A concrete topic is open: leave the cards grid for the detail
1829
+ * (tabs bar + table + back button). No-op in tabs-only mode. */
1830
+ show_topic_detail(gobj);
1831
+ }
1832
+
1833
+ refresh_toolbar_buttons(gobj); /* the new tab may or may not be a tree */
1834
+ return 0;
1835
+ }
1836
+
1837
+ /************************************************************
1838
+ * Show the routed topic-info panel (card info icon / .../<topic>/info).
1839
+ ************************************************************/
1840
+ function ac_show_topic_info(gobj, event, kw, src)
1841
+ {
1842
+ show_topic_info(gobj, kw && kw.topic);
1843
+ return 0;
1844
+ }
1845
+
1846
+ /************************************************************
1847
+ * Highlight the clicked topic card in the landing grid (single
1848
+ * selection). Visual only — navigation, when the click was on an
1849
+ * icon, is the anchor's own hash routing.
1850
+ ************************************************************/
1851
+ function ac_select_topic_card(gobj, event, kw, src)
1852
+ {
1853
+ let topic = kw && kw.topic;
1854
+ gobj.priv._selected_card_topic = topic || null;
1855
+ let $container = gobj_read_attr(gobj, "$container");
1856
+ if(!$container) {
1857
+ return 0;
946
1858
  }
1859
+ $container.querySelectorAll(".TREEDB_TOPIC_CARD").forEach(function($c) {
1860
+ $c.classList.toggle("is-selected", $c.dataset.topic === topic);
1861
+ });
1862
+ return 0;
1863
+ }
947
1864
 
1865
+ /************************************************************
1866
+ * Toggle the landing between the cards grid and the schema graph.
1867
+ * The landing is a POSITION, so this NAVIGATES (a real hash push via
1868
+ * the host-supplied `landing_routes`) rather than mutating in-view
1869
+ * state — the route change then drives the switch (EV_SET_LANDING_VIEW),
1870
+ * so it is URL-addressable, F5-safe and Back-friendly (ROUTING.md §3).
1871
+ * Falls back to an in-view flip only when no routes were supplied.
1872
+ ************************************************************/
1873
+ function ac_toggle_landing_view(gobj, event, kw, src)
1874
+ {
1875
+ let priv = gobj.priv;
1876
+ let target = (priv._landing_view === "schema") ? "cards" : "schema";
1877
+ let routes = gobj_read_attr(gobj, "landing_routes");
1878
+ let href = is_object(routes) ? routes[target] : null;
1879
+ if(href && typeof window !== "undefined") {
1880
+ window.location.hash = href; /* push; route drives the switch */
1881
+ return 0;
1882
+ }
1883
+ /* Legacy: no routes → flip in-view (not URL-addressable). */
1884
+ set_landing_view(gobj, target);
1885
+ return 0;
1886
+ }
1887
+
1888
+ /************************************************************
1889
+ * Set the landing sub-view ("cards" | "schema") and render it.
1890
+ * Driven by the route (EV_SET_LANDING_VIEW) or the legacy toggle.
1891
+ ************************************************************/
1892
+ function set_landing_view(gobj, view)
1893
+ {
1894
+ gobj.priv._landing_view = (view === "schema") ? "schema" : "cards";
1895
+ show_topics_landing(gobj); /* syncs the toggle + shows the pane */
1896
+ }
1897
+
1898
+ /************************************************************
1899
+ * Route → landing sub-view (.../db/<sel>/schema or bare tab).
1900
+ ************************************************************/
1901
+ function ac_set_landing_view(gobj, event, kw, src)
1902
+ {
1903
+ set_landing_view(gobj, kw && kw.view);
1904
+ return 0;
1905
+ }
1906
+
1907
+ /************************************************************
1908
+ * Back from a topic to the cards-landing grid (the section index).
1909
+ ************************************************************/
1910
+ function ac_back_to_topics(gobj, event, kw, src)
1911
+ {
1912
+ show_topics_landing(gobj);
1913
+ /* Tell the host the topic segment is gone so a reload re-lands on the
1914
+ * grid (empty topic ⇒ the host drops the <topic> from the URL). */
1915
+ gobj_publish_event(gobj, "EV_TOPIC_SELECTED", {topic: ""});
1916
+ return 0;
1917
+ }
1918
+
1919
+ /************************************************************
1920
+ * The host (C_TREEDB_VIEW) forwards the backend transport edges here so the
1921
+ * toolbar can disable the JSON viewers the moment the session drops (and
1922
+ * re-enable them on reconnect) — the library view must not subscribe to the
1923
+ * C_IEVENT_CLI itself (that forwards the subscription upstream and breaks
1924
+ * the session).
1925
+ ************************************************************/
1926
+ function ac_transport_state(gobj, event, kw, src)
1927
+ {
1928
+ refresh_toolbar_buttons(gobj, !!(kw && kw.connected));
948
1929
  return 0;
949
1930
  }
950
1931
 
@@ -1147,6 +2128,48 @@ function ac_refresh_topic(gobj, event, kw, src)
1147
2128
  return 0;
1148
2129
  }
1149
2130
 
2131
+ /********************************************
2132
+ * Open the raw-tranger JSON viewer (whole service).
2133
+ ********************************************/
2134
+ function ac_open_json(gobj, event, kw, src)
2135
+ {
2136
+ open_json_viewer(gobj);
2137
+ return 0;
2138
+ }
2139
+
2140
+ /********************************************
2141
+ * The viewer asked to load a collapsed subtree (tranger mode only):
2142
+ * re-issue print-tranger for that path.
2143
+ ********************************************/
2144
+ function ac_json_expand_path(gobj, event, kw, src)
2145
+ {
2146
+ request_print_tranger(gobj, (kw && kw.path) || "");
2147
+ return 0;
2148
+ }
2149
+
2150
+ /********************************************
2151
+ * The JSON viewer was dismissed / torn down: release it, clear refs.
2152
+ ********************************************/
2153
+ function ac_json_closed(gobj, event, kw, src)
2154
+ {
2155
+ let priv = gobj.priv;
2156
+ let jv = priv.json_gobj;
2157
+ priv.json_gobj = null;
2158
+ priv.json_win = null;
2159
+ priv.json_modal = null;
2160
+ if(jv && is_gobj(jv)) {
2161
+ try {
2162
+ /* STOP before destroy — the viewer was STARTED in open_json_viewer
2163
+ * (see close_json_viewer for the full rationale). */
2164
+ gobj_stop(jv);
2165
+ gobj_destroy(jv);
2166
+ } catch(e) {
2167
+ log_warning(`${gobj_short_name(gobj)}: already gone: ${e}`);
2168
+ }
2169
+ }
2170
+ return 0;
2171
+ }
2172
+
1150
2173
 
1151
2174
 
1152
2175
 
@@ -1191,8 +2214,17 @@ function create_gclass(gclass_name)
1191
2214
  ["EV_UPDATE_RECORD", ac_update_record, null],
1192
2215
  ["EV_DELETE_RECORD", ac_delete_record, null],
1193
2216
  ["EV_REFRESH_TOPIC", ac_refresh_topic, null],
2217
+ ["EV_OPEN_JSON", ac_open_json, null],
2218
+ ["EV_EXPAND_PATH", ac_json_expand_path, null],
2219
+ ["EV_JSON_CLOSED", ac_json_closed, null],
1194
2220
  ["EV_SHOW", ac_show, null],
2221
+ ["EV_SHOW_TOPIC_INFO", ac_show_topic_info, null],
2222
+ ["EV_SELECT_TOPIC_CARD", ac_select_topic_card, null],
2223
+ ["EV_TOGGLE_LANDING_VIEW", ac_toggle_landing_view, null],
2224
+ ["EV_SET_LANDING_VIEW", ac_set_landing_view, null],
1195
2225
  ["EV_HIDE", ac_hide, null],
2226
+ ["EV_BACK_TO_TOPICS", ac_back_to_topics, null],
2227
+ ["EV_TRANSPORT_STATE", ac_transport_state, null],
1196
2228
  ]]
1197
2229
  ];
1198
2230
 
@@ -1208,8 +2240,17 @@ function create_gclass(gclass_name)
1208
2240
  ["EV_UPDATE_RECORD", 0],
1209
2241
  ["EV_DELETE_RECORD", 0],
1210
2242
  ["EV_REFRESH_TOPIC", 0],
2243
+ ["EV_OPEN_JSON", 0],
2244
+ ["EV_EXPAND_PATH", 0],
2245
+ ["EV_JSON_CLOSED", 0],
1211
2246
  ["EV_SHOW", 0],
2247
+ ["EV_SHOW_TOPIC_INFO", 0],
2248
+ ["EV_SELECT_TOPIC_CARD", 0],
2249
+ ["EV_TOGGLE_LANDING_VIEW", 0],
2250
+ ["EV_SET_LANDING_VIEW", 0],
1212
2251
  ["EV_HIDE", 0],
2252
+ ["EV_BACK_TO_TOPICS", 0],
2253
+ ["EV_TRANSPORT_STATE", 0],
1213
2254
  ["EV_TOPIC_SELECTED",
1214
2255
  event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS]
1215
2256
  ];