@yuneta/gobj-ui 7.23.161 → 7.23.163

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.
@@ -258,6 +258,10 @@ let PRIVATE_DATA = {
258
258
  cell_file_win: null, // C_YUI_WINDOW presenting it (laptop)
259
259
  _pending_assets: null, // req_id -> {target, col, id, slot, timer}
260
260
  _asset_seq: 0, // correlation id of an asset request
261
+ parent_filter: null, // {fkey, parent_topic, parent_id, hook}: rows linked to one record
262
+ search_term: "", // the search box, lower-cased ("" = none)
263
+ $parent_filter: null, // the chip that says the parent filter is on
264
+ hook_choice_modal: null, // { close } of "which child topic?" (a hook with several)
261
265
  };
262
266
 
263
267
  let __gclass__ = null;
@@ -459,6 +463,7 @@ function mt_stop(gobj)
459
463
  close_schema_dialog(gobj);
460
464
  close_cell_json_dialog(gobj);
461
465
  close_cell_file_dialog(gobj);
466
+ close_hook_choice(gobj);
462
467
  drop_pending_assets(gobj);
463
468
  table__destroy(gobj);
464
469
  }
@@ -476,6 +481,7 @@ function mt_destroy(gobj)
476
481
  close_schema_dialog(gobj);
477
482
  close_cell_json_dialog(gobj);
478
483
  close_cell_file_dialog(gobj);
484
+ close_hook_choice(gobj);
479
485
  drop_pending_assets(gobj);
480
486
  destroy_ui(gobj);
481
487
  }
@@ -898,6 +904,31 @@ function build_ui(gobj)
898
904
  $container.insertBefore(bar.$el, $container.querySelector(`#${table_id}`));
899
905
  }
900
906
 
907
+ /* The parent filter, when a hook of another row opened this table
908
+ * (EV_FILTER_BY_PARENT). Hidden until then. The topic is a KEY (it is
909
+ * translated like the tab that names it) and the id is DATA. */
910
+ let $parent_filter = createElement2(
911
+ ['div', {class: 'TREEDB_PARENT_FILTER is-hidden', style: 'flex:none;'}, [
912
+ ['span', {class: 'PARENT_FILTER_LABEL', i18n: 'filtered by'}, 'filtered by'],
913
+ ['span', {class: 'PARENT_FILTER_TOPIC'}, ''],
914
+ ['strong', {class: 'PARENT_FILTER_ID'}, ''],
915
+ ['button', {
916
+ class: 'delete PARENT_FILTER_CLEAR',
917
+ type: 'button',
918
+ title: t('clear filter'),
919
+ 'data-i18n-title': 'clear filter',
920
+ 'aria-label': t('clear filter'),
921
+ 'data-i18n-aria-label': 'clear filter'
922
+ }, [], {
923
+ click: () => {
924
+ gobj_send_event(gobj, "EV_CLEAR_PARENT_FILTER", {}, gobj);
925
+ }
926
+ }]
927
+ ]]
928
+ );
929
+ gobj.priv.$parent_filter = $parent_filter;
930
+ $container.insertBefore($parent_filter, $container.querySelector(`#${table_id}`));
931
+
901
932
  let $toolbar_slot = $container.querySelector('.toolbar_tabulator_table');
902
933
  if($table_toolbar instanceof Element) {
903
934
  $toolbar_slot.appendChild($table_toolbar);
@@ -1741,6 +1772,7 @@ function create_tabulator(gobj)
1741
1772
  * one row. */
1742
1773
  yui_tabulator_name_row_selects(tabulator, t);
1743
1774
  tabulator._ready = true;
1775
+ apply_row_filters(gobj);
1744
1776
  update_rowcount();
1745
1777
  /* La ✕ de cada filtro de cabecera. Un filtro se quita borrando lo
1746
1778
  * escrito, y con varias columnas filtradas volver a la tabla entera
@@ -2031,8 +2063,14 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
2031
2063
  let items = treedb_hook_data_size(value);
2032
2064
 
2033
2065
  if(items > 0) {
2066
+ /* An HTML STRING, so the name is escaped by hand; a language
2067
+ * change re-runs setColumns(), which re-runs this. */
2068
+ let hook_title = String(t("show linked records"))
2069
+ .replace(/&/g, "&").replace(/"/g, """)
2070
+ .replace(/</g, "&lt;").replace(/>/g, "&gt;");
2034
2071
  value = [
2035
2072
  '<a class="hook_cell" ',
2073
+ `title="${hook_title}" aria-label="${hook_title}" `,
2036
2074
  `data-row_id="${row.id}" `,
2037
2075
  `data-col_id="${col.id}" > `,
2038
2076
  '<span style="" class="icon yi-eye"></span>',
@@ -2892,6 +2930,181 @@ function close_cell_file_dialog(gobj)
2892
2930
  teardown_cell_file(gobj);
2893
2931
  }
2894
2932
 
2933
+ /************************************************************
2934
+ * Ask the host to open the rows of `topic_name` whose `fkey`
2935
+ * names this row -- what a hook of this row links.
2936
+ ************************************************************/
2937
+ function publish_open_linked(gobj, topic_name, fkey, parent_id, hook)
2938
+ {
2939
+ gobj_publish_event(gobj, "EV_OPEN_LINKED", {
2940
+ topic_name: topic_name,
2941
+ fkey: fkey,
2942
+ parent_topic: gobj_read_str_attr(gobj, "topic_name"),
2943
+ parent_id: parent_id,
2944
+ hook: hook
2945
+ });
2946
+ }
2947
+
2948
+ /************************************************************
2949
+ * "Which child topic?" -- for a hook whose children live in
2950
+ * more than one. The standardized dialog, so Escape, Back and
2951
+ * a click outside close it like every other one.
2952
+ ************************************************************/
2953
+ function open_hook_choice_dialog(gobj, targets, parent_id, hook)
2954
+ {
2955
+ close_hook_choice(gobj);
2956
+ let priv = gobj.priv;
2957
+ let shell = yui_shell_of(gobj);
2958
+ if(!shell) {
2959
+ log_error(`${gobj_short_name(gobj)}: no shell, cannot ask which topic to open`);
2960
+ return;
2961
+ }
2962
+ /* The topic names are KEYS, translated like the tabs that name them. */
2963
+ let $body = createElement2(
2964
+ ['div', {class: 'TREEDB_HOOK_CHOICE'}, targets.map((tg) => {
2965
+ return ['button', {
2966
+ class: 'button HOOK_CHOICE_TOPIC',
2967
+ type: 'button',
2968
+ title: t(tg.topic_name),
2969
+ 'data-i18n-title': tg.topic_name,
2970
+ 'aria-label': t(tg.topic_name),
2971
+ 'data-i18n-aria-label': tg.topic_name
2972
+ }, [
2973
+ ['span', {i18n: tg.topic_name}, tg.topic_name]
2974
+ ], {
2975
+ click: () => {
2976
+ gobj_send_event(gobj, "EV_CHOOSE_LINKED", {
2977
+ topic_name: tg.topic_name,
2978
+ fkey: tg.fkey,
2979
+ parent_id: parent_id,
2980
+ hook: hook
2981
+ }, gobj);
2982
+ }
2983
+ }];
2984
+ })]
2985
+ );
2986
+ priv.hook_choice_modal = yui_shell_show_modal(shell, $body, {
2987
+ dialog: true,
2988
+ logical_class: "TREEDB_HOOK_CHOICE_SHEET",
2989
+ title: "choose a topic",
2990
+ t: t,
2991
+ on_close: function() {
2992
+ priv.hook_choice_modal = null;
2993
+ }
2994
+ });
2995
+ }
2996
+
2997
+ /************************************************************
2998
+ * Close "which child topic?" if it is open.
2999
+ ************************************************************/
3000
+ function close_hook_choice(gobj)
3001
+ {
3002
+ let priv = gobj.priv;
3003
+ if(priv.hook_choice_modal) {
3004
+ let modal = priv.hook_choice_modal;
3005
+ priv.hook_choice_modal = null;
3006
+ modal.close();
3007
+ }
3008
+ }
3009
+
3010
+ /************************************************************
3011
+ * Does this row's `pf.fkey` name the parent record? The fkey
3012
+ * arrives as one ref, a list of them or a dict keyed by ref,
3013
+ * each in any of the shapes treedb_decoder_fkey() reads. The
3014
+ * topic and the hook are compared when the ref carries them:
3015
+ * one row can hang from the same parent through two hooks.
3016
+ ************************************************************/
3017
+ function row_links_to(gobj, data, pf)
3018
+ {
3019
+ let col = get_schema_col(gobj, pf.fkey);
3020
+ let v = data? data[pf.fkey] : null;
3021
+ if(!col || v === null || v === undefined || v === "") {
3022
+ return false;
3023
+ }
3024
+ let refs;
3025
+ if(is_array(v)) {
3026
+ refs = v;
3027
+ } else if(is_object(v) && v.id === undefined) {
3028
+ refs = Object.keys(v);
3029
+ } else {
3030
+ refs = [v];
3031
+ }
3032
+ for(let ref of refs) {
3033
+ if(ref === null || ref === undefined || ref === "") {
3034
+ continue;
3035
+ }
3036
+ let fk = treedb_decoder_fkey(col, ref);
3037
+ if(!fk || String(fk.id) !== pf.parent_id) {
3038
+ continue;
3039
+ }
3040
+ if(fk.topic_name && pf.parent_topic && fk.topic_name !== pf.parent_topic) {
3041
+ continue;
3042
+ }
3043
+ if(fk.hook_name && pf.hook && fk.hook_name !== pf.hook) {
3044
+ continue;
3045
+ }
3046
+ return true;
3047
+ }
3048
+ return false;
3049
+ }
3050
+
3051
+ /************************************************************
3052
+ * The ONE programmatic filter of the table: the parent filter
3053
+ * AND the search term. Kept as state and applied here, because
3054
+ * Tabulator's setFilter() replaces every programmatic filter.
3055
+ * Before `tableBuilt` the state waits; tableBuilt applies it.
3056
+ ************************************************************/
3057
+ function apply_row_filters(gobj)
3058
+ {
3059
+ let priv = gobj.priv;
3060
+ let tabulator = gobj_read_attr(gobj, "tabulator");
3061
+ if(!tabulator) {
3062
+ log_error(`${gobj_short_name(gobj)}: no table to filter`);
3063
+ return;
3064
+ }
3065
+ if(!tabulator._ready) {
3066
+ return; /* tableBuilt applies it */
3067
+ }
3068
+ let term = priv.search_term || "";
3069
+ let pf = priv.parent_filter;
3070
+ if(!term && !pf) {
3071
+ tabulator.clearFilter();
3072
+ return;
3073
+ }
3074
+ tabulator.setFilter(function(data) {
3075
+ if(pf && !row_links_to(gobj, data, pf)) {
3076
+ return false;
3077
+ }
3078
+ /* A treedb row is not flat: an fkey arrives as a list of
3079
+ * objects. `row_matches()` walks into them and reads only the
3080
+ * `id` of an fkey. See yui_row_search.js. */
3081
+ return !term || row_matches(data, term);
3082
+ });
3083
+ }
3084
+
3085
+ /************************************************************
3086
+ * Show or hide the chip that says the parent filter is on.
3087
+ ************************************************************/
3088
+ function render_parent_filter(gobj)
3089
+ {
3090
+ let priv = gobj.priv;
3091
+ let $chip = priv.$parent_filter;
3092
+ if(!$chip) {
3093
+ log_error(`${gobj_short_name(gobj)}: no parent-filter chip to paint`);
3094
+ return;
3095
+ }
3096
+ let pf = priv.parent_filter;
3097
+ if(!pf) {
3098
+ $chip.classList.add("is-hidden");
3099
+ return;
3100
+ }
3101
+ let $topic = $chip.querySelector(".PARENT_FILTER_TOPIC");
3102
+ $topic.setAttribute("data-i18n", pf.parent_topic);
3103
+ $topic.textContent = t(pf.parent_topic);
3104
+ $chip.querySelector(".PARENT_FILTER_ID").textContent = pf.parent_id;
3105
+ $chip.classList.remove("is-hidden");
3106
+ }
3107
+
2895
3108
  /************************************************************
2896
3109
  * True if the topic's pkey col carries the "rowid" flag.
2897
3110
  ************************************************************/
@@ -3302,84 +3515,6 @@ function build_fkey_ref(gobj, col, value)
3302
3515
  return refs;
3303
3516
  }
3304
3517
 
3305
- /***************************************************************************
3306
- * TODO con límite máximo o máximo height o con scroll
3307
- * en un gobj propio para gestionar los datos en "page"s
3308
- * que los hook no vengan rellenos si son muchos y que se puedan gestionar
3309
- * con un gobj
3310
- ***************************************************************************/
3311
- function show_dropdown_popup_menu(gobj, x, y, items, callback)
3312
- {
3313
- let $element = createElement2([
3314
- 'div', {class: 'dropdown popup' }, [
3315
- ['div', {
3316
- class: 'dropdown-menu', role: 'menu', style: 'min-width:4rem; border: 2px solid var(--bulma-border); padding: 0px;'
3317
- }, [
3318
- ['div', { class: 'dropdown-content', style: 'padding: 0;' }, []]
3319
- ]]
3320
- ], {
3321
- 'click': (evt) => {
3322
- evt.stopPropagation();
3323
- destroyModal();
3324
- if(callback) {
3325
- callback(evt);
3326
- }
3327
- }
3328
- }
3329
- ]);
3330
-
3331
- const destroyModal = () => {
3332
- $element.classList.remove('is-active');
3333
- $element.parentNode.removeChild($element);
3334
- };
3335
-
3336
- let $dropdown_content = $element.querySelector('.dropdown-content');
3337
-
3338
- let ids = kwid_get_ids(gobj, items);
3339
- for(let id of ids) {
3340
- let $item = createElement2(
3341
- ['a', {class: 'dropdown-item flex-horizontal-section', 'data-value':`${id}`, style:'margin:0px;'}, [
3342
- ['span', {i18n: `${id}`}, `${id}`]
3343
- ], {
3344
- 'click': (evt) => {
3345
- evt.stopPropagation();
3346
- destroyModal();
3347
- if(callback) {
3348
- callback(evt, this.dataset.value);
3349
- }
3350
- }
3351
- }]
3352
- );
3353
- $dropdown_content.appendChild($item);
3354
- }
3355
-
3356
- refresh_language($element, t);
3357
-
3358
- /*
3359
- * Add to popup layer
3360
- */
3361
- popup_mount_layer(gobj).appendChild($element);
3362
-
3363
- /*
3364
- * Set position
3365
- */
3366
- $element.style.position = "absolute";
3367
- $element.style.top = y + "px";
3368
- $element.style.left = x + "px";
3369
-
3370
- /*
3371
- * Show
3372
- */
3373
- $element.classList.add('is-active');
3374
-
3375
- /*
3376
- * Set focus
3377
- */
3378
- let $with_focus = $element.querySelector('.with-focus');
3379
- if($with_focus) {
3380
- $with_focus.focus();
3381
- }
3382
- }
3383
3518
 
3384
3519
 
3385
3520
 
@@ -4117,17 +4252,78 @@ function ac_change_locale(gobj, event, kw, src)
4117
4252
  ************************************************************/
4118
4253
  function ac_show_hook_data(gobj, event, kw, src)
4119
4254
  {
4120
- let webix = gobj_command(gobj_parent(gobj), "get_topic_data", kw, gobj);
4255
+ /* The hook's mapping says where its children live: {child_topic:
4256
+ * fkey_col}. Opening that topic, filtered, replaced a popup listing
4257
+ * every child id -- 5675 rows in a box with no height, no scroll and
4258
+ * no way out but a click inside it, whose ids did nothing. */
4259
+ let col = get_schema_col(gobj, kw.col_id);
4260
+ let hook = (col && is_object(col.hook))? col.hook : null;
4261
+ if(!hook || kw.row_id === undefined || kw.row_id === null) {
4262
+ log_error(`${gobj_short_name(gobj)}: hook '${kw.col_id}' names no child topic to open`);
4263
+ return -1;
4264
+ }
4265
+ let targets = Object.keys(hook).map((topic) => {
4266
+ return {topic_name: topic, fkey: hook[topic]};
4267
+ });
4268
+ if(targets.length === 1) {
4269
+ publish_open_linked(gobj, targets[0].topic_name, targets[0].fkey,
4270
+ String(kw.row_id), kw.col_id);
4271
+ return 0;
4272
+ }
4273
+ open_hook_choice_dialog(gobj, targets, String(kw.row_id), kw.col_id);
4274
+ return 0;
4275
+ }
4121
4276
 
4122
- let row = kwid_find_one_record(gobj, webix.data, kw.row_id, null);
4123
- if(row) {
4124
- let cell = row[kw.col_id];
4125
- /*
4126
- * WARNING TODO hooks can have millions of kids
4127
- */
4128
- show_dropdown_popup_menu(gobj, kw.click_x, kw.click_y, cell);
4277
+ /************************************************************
4278
+ * A hook with several child topics: pick one.
4279
+ * {topic_name, fkey, parent_id, hook}
4280
+ ************************************************************/
4281
+ function ac_choose_linked(gobj, event, kw, src)
4282
+ {
4283
+ close_hook_choice(gobj);
4284
+ publish_open_linked(gobj, kw.topic_name, kw.fkey, kw.parent_id, kw.hook);
4285
+ return 0;
4286
+ }
4287
+
4288
+ /************************************************************
4289
+ * Show only the rows linked to one record of another topic.
4290
+ * Sent by the host when a hook of that record was clicked.
4291
+ * {fkey, parent_topic, parent_id, hook}
4292
+ *
4293
+ * Over the rows the table HOLDS: with `with_remote_paging` that
4294
+ * is one page, which is one of the reasons that flag stays off.
4295
+ ************************************************************/
4296
+ function ac_filter_by_parent(gobj, event, kw, src)
4297
+ {
4298
+ let priv = gobj.priv;
4299
+ if(!kw || !kw.fkey || kw.parent_id === undefined || kw.parent_id === null) {
4300
+ log_error(`${gobj_short_name(gobj)}: a parent filter with no fkey or no parent`);
4301
+ return -1;
4129
4302
  }
4303
+ if(!get_schema_col(gobj, kw.fkey)) {
4304
+ log_error(`${gobj_short_name(gobj)}: parent filter on '${kw.fkey}', ` +
4305
+ `which is not a column of '${gobj_read_str_attr(gobj, "topic_name")}'`);
4306
+ return -1;
4307
+ }
4308
+ priv.parent_filter = {
4309
+ fkey: kw.fkey,
4310
+ parent_topic: kw.parent_topic || "",
4311
+ parent_id: String(kw.parent_id),
4312
+ hook: kw.hook || ""
4313
+ };
4314
+ render_parent_filter(gobj);
4315
+ apply_row_filters(gobj);
4316
+ return 0;
4317
+ }
4130
4318
 
4319
+ /************************************************************
4320
+ * The ✕ of the parent filter: back to the whole topic.
4321
+ ************************************************************/
4322
+ function ac_clear_parent_filter(gobj, event, kw, src)
4323
+ {
4324
+ gobj.priv.parent_filter = null;
4325
+ render_parent_filter(gobj);
4326
+ apply_row_filters(gobj);
4131
4327
  return 0;
4132
4328
  }
4133
4329
 
@@ -4479,20 +4675,12 @@ function ac_search(gobj, event, kw, src)
4479
4675
  return -1;
4480
4676
  }
4481
4677
 
4482
- let term = ((kw && kw.text) || "").toLowerCase();
4483
- if(!term) {
4484
- tabulator.clearFilter();
4485
- return 0;
4486
- }
4487
-
4488
- /* A treedb row is not flat: an fkey arrives as a list of objects, and
4489
- * `String()` of that is "[object Object]" -- so searching for the
4490
- * workshop of a meter, which is where the value a person has in mind
4491
- * lives, never found anything. `row_matches()` walks into them, and
4492
- * reads only the `id` of an fkey. See yui_row_search.js. */
4493
- tabulator.setFilter(function(data) {
4494
- return row_matches(data, term);
4495
- });
4678
+ /* Kept, and applied together with the parent filter: Tabulator's
4679
+ * setFilter() REPLACES every programmatic filter and clearFilter()
4680
+ * drops them all, so the search box used to wipe the parent filter
4681
+ * and the parent filter would have wiped the search. */
4682
+ gobj.priv.search_term = ((kw && kw.text) || "").toLowerCase();
4683
+ apply_row_filters(gobj);
4496
4684
 
4497
4685
  return 0;
4498
4686
  }
@@ -4726,6 +4914,9 @@ function create_gclass(gclass_name)
4726
4914
  ["EV_COPY_ROWS", ac_copy_rows, null],
4727
4915
  ["EV_PASTE_ROWS", ac_paste_rows, null],
4728
4916
  ["EV_SHOW_HOOK_DATA", ac_show_hook_data, null],
4917
+ ["EV_CHOOSE_LINKED", ac_choose_linked, null],
4918
+ ["EV_FILTER_BY_PARENT", ac_filter_by_parent, null],
4919
+ ["EV_CLEAR_PARENT_FILTER", ac_clear_parent_filter, null],
4729
4920
  ["EV_SHOW_CELL_JSON", ac_show_cell_json, null],
4730
4921
  ["EV_SHOW_CELL_FILE", ac_show_cell_file, null],
4731
4922
  ["EV_ASSET_LOADED", ac_asset_loaded, null],
@@ -4770,7 +4961,11 @@ function create_gclass(gclass_name)
4770
4961
  ["EV_CLEAR_SELECTION", 0],
4771
4962
  ["EV_SELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
4772
4963
  ["EV_UNSELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
4773
- ["EV_SHOW_HOOK_DATA", event_flag_t.EVF_OUTPUT_EVENT],
4964
+ ["EV_SHOW_HOOK_DATA", 0],
4965
+ ["EV_CHOOSE_LINKED", 0],
4966
+ ["EV_FILTER_BY_PARENT", 0],
4967
+ ["EV_CLEAR_PARENT_FILTER", 0],
4968
+ ["EV_OPEN_LINKED", event_flag_t.EVF_OUTPUT_EVENT],
4774
4969
  ["EV_SHOW_CELL_JSON", 0],
4775
4970
  ["EV_SHOW_CELL_FILE", 0],
4776
4971
  ["EV_ASSET_LOADED", 0],
@@ -74,6 +74,21 @@ import {t} from "i18next";
74
74
  ***************************************************************/
75
75
  const GCLASS_NAME = "C_YUI_TREEDB_TOPICS";
76
76
 
77
+ /*
78
+ * How a topic TABLE reads its rows. `hook_size`: a hook comes back as
79
+ * its COUNT, `[{"size": N}]`, not as the id of every child. The cell
80
+ * only ever showed the number, and a click on it opens the child topic
81
+ * filtered (EV_OPEN_LINKED) -- so the ids were downloaded for nothing:
82
+ * every device id, once per device type, on every load of that topic.
83
+ *
84
+ * Only the table LOADS use it. The writes keep their own options, and
85
+ * the node events still carry whole hooks (c_node publishes them with
86
+ * `list_dict`, whatever a reader asked for); the cell counts either
87
+ * shape (`treedb_hook_data_size`), and so does a delete's question
88
+ * (`delete_impact`'s ref_count).
89
+ */
90
+ const TABLE_READ_OPTIONS = {list_dict: true, hook_size: true};
91
+
77
92
  /***************************************************************
78
93
  * Data
79
94
  ***************************************************************/
@@ -1316,9 +1331,7 @@ function get_nodes(gobj, topic_name)
1316
1331
  gobj,
1317
1332
  treedb_name,
1318
1333
  topic_name,
1319
- {
1320
- list_dict: true
1321
- }
1334
+ Object.assign({}, TABLE_READ_OPTIONS)
1322
1335
  );
1323
1336
  }
1324
1337
 
@@ -2371,7 +2384,7 @@ function ac_request_page(gobj, event, kw, src)
2371
2384
  gobj,
2372
2385
  gobj_read_str_attr(gobj, "treedb_name"),
2373
2386
  topic_name,
2374
- {list_dict: true},
2387
+ Object.assign({}, TABLE_READ_OPTIONS),
2375
2388
  {from: kw.from, limit: kw.limit, req_id: kw.req_id}
2376
2389
  );
2377
2390
 
@@ -2384,6 +2397,47 @@ function ac_request_page(gobj, event, kw, src)
2384
2397
  return 0;
2385
2398
  }
2386
2399
 
2400
+ /********************************************
2401
+ * Event from formtable: open the records a hook of one row links --
2402
+ * the CHILD topic's table, filtered to the rows whose fkey names that
2403
+ * row. A hook is the question "which records hang from this one", and
2404
+ * the child's table is where that answer can be read: it pages, it
2405
+ * searches, it opens a record and it exports -- which a list of ids in
2406
+ * a popup could not do past a few dozen of them.
2407
+ * {
2408
+ * topic_name: the child topic
2409
+ * fkey: its fkey column that points back
2410
+ * parent_topic: the topic of the row the hook is on
2411
+ * parent_id: that row's id
2412
+ * hook: the hook column
2413
+ * }
2414
+ ********************************************/
2415
+ function ac_open_linked(gobj, event, kw, src)
2416
+ {
2417
+ let child = get_gobj_formtable(gobj, kw.topic_name);
2418
+ if(!child) {
2419
+ /* A child topic this view does not show: a system topic in a
2420
+ * user view, or one the schema names and the backend did not
2421
+ * describe. */
2422
+ log_error(`${gobj_short_name(gobj)}: cannot open '${kw.topic_name}', ` +
2423
+ `linked from '${kw.parent_topic}^${kw.parent_id}': that topic is not in this view`);
2424
+ return -1;
2425
+ }
2426
+
2427
+ /* Through the same entry point as a tab click: the switch is local
2428
+ * and the host mirrors it into the URL, so Back returns to the
2429
+ * topic the hook was clicked in. */
2430
+ select_topic_by_id(gobj, gobj_name(child));
2431
+
2432
+ gobj_send_event(child, "EV_FILTER_BY_PARENT", {
2433
+ fkey: kw.fkey,
2434
+ parent_topic: kw.parent_topic,
2435
+ parent_id: kw.parent_id,
2436
+ hook: kw.hook
2437
+ }, gobj);
2438
+ return 0;
2439
+ }
2440
+
2387
2441
  /********************************************
2388
2442
  * Event from formtable: the bytes of one asset a `file` column names,
2389
2443
  * for the form's preview or the table's file popup.
@@ -2551,9 +2605,7 @@ function ac_refresh_topic(gobj, event, kw, src)
2551
2605
  return 0;
2552
2606
  }
2553
2607
 
2554
- let options = {
2555
- list_dict: true
2556
- };
2608
+ let options = Object.assign({}, TABLE_READ_OPTIONS);
2557
2609
 
2558
2610
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
2559
2611
 
@@ -2676,6 +2728,7 @@ function create_gclass(gclass_name)
2676
2728
  ["EV_UPDATE_FIELD", ac_update_field, null],
2677
2729
  ["EV_REQUEST_PAGE", ac_request_page, null],
2678
2730
  ["EV_REQUEST_ASSET", ac_request_asset, null],
2731
+ ["EV_OPEN_LINKED", ac_open_linked, null],
2679
2732
  ["EV_DELETE_RECORD", ac_delete_record, null],
2680
2733
  ["EV_REFRESH_TOPIC", ac_refresh_topic, null],
2681
2734
  ["EV_OPEN_JSON", ac_open_json, null],
@@ -2706,6 +2759,7 @@ function create_gclass(gclass_name)
2706
2759
  ["EV_UPDATE_FIELD", 0],
2707
2760
  ["EV_REQUEST_PAGE", 0],
2708
2761
  ["EV_REQUEST_ASSET", 0],
2762
+ ["EV_OPEN_LINKED", 0],
2709
2763
  ["EV_DELETE_RECORD", 0],
2710
2764
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT|
2711
2765
  event_flag_t.EVF_NO_WARN_SUBS],
@@ -31,6 +31,13 @@
31
31
  ***************************************************************/
32
32
  function ref_count(value)
33
33
  {
34
+ /* A hook read with `hook_size` is not its children but their COUNT:
35
+ * `[{"size": N}]`. Counted as a one-element list it said a parent of
36
+ * 5675 children took ONE with it. */
37
+ if(Array.isArray(value) && value.length === 1 && value[0] &&
38
+ typeof value[0] === "object" && typeof value[0].size === "number") {
39
+ return value[0].size;
40
+ }
34
41
  if(Array.isArray(value)) {
35
42
  return value.length;
36
43
  }
@@ -21,6 +21,14 @@ describe("ref_count", () => {
21
21
  expect(ref_count(undefined)).toBe(0);
22
22
  expect(ref_count(7)).toBe(0);
23
23
  });
24
+
25
+ it("reads a hook read with hook_size as its count", () => {
26
+ expect(ref_count([{size: 5675}])).toBe(5675);
27
+ expect(ref_count([{size: 0}])).toBe(0);
28
+ expect(delete_impact(DESC, {id: "a", users: [{size: 12}]}).children).toBe(12);
29
+ /* one real child that happens to be an object is still ONE */
30
+ expect(ref_count([{id: "u1"}])).toBe(1);
31
+ });
24
32
  });
25
33
 
26
34
  describe("delete_impact", () => {