@yuneta/gobj-ui 7.23.160 → 7.23.162

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
  ************************************************************/
@@ -2928,13 +3141,19 @@ function show_edit_form(gobj, row, index)
2928
3141
  * Does a click on this column already mean something else?
2929
3142
  *
2930
3143
  * The `_` columns are the table's own scaffolding (the selection
2931
- * checkbox, the Op buttons); a hook opens its children and a col
2932
- * holding a json document opens it in a viewer. Anything else is
2933
- * plain data, and a click on it means "show me this record".
3144
+ * checkbox, the Op buttons); a hook opens its children, a col
3145
+ * holding a json document opens it in a viewer, and a `file` col
3146
+ * opens its asset in a popup. Anything else is plain data, and a
3147
+ * click on it means "show me this record".
3148
+ *
3149
+ * `file` was missing when its popup arrived (7.23.159), so a click
3150
+ * on a photo opened the popup AND the record on top of it: Tabulator
3151
+ * fires `rowClick` from its own dispatcher, and the cell's
3152
+ * stopPropagation() does not reach it.
2934
3153
  ************************************************************/
2935
3154
  const CELL_WITH_ACTION = [
2936
3155
  "hook", "object", "dict", "template", "array", "list",
2937
- "coordinates", "blob", "gbuffer"
3156
+ "coordinates", "blob", "gbuffer", "file"
2938
3157
  ];
2939
3158
 
2940
3159
  function cell_has_its_own_action(gobj, field)
@@ -3296,84 +3515,6 @@ function build_fkey_ref(gobj, col, value)
3296
3515
  return refs;
3297
3516
  }
3298
3517
 
3299
- /***************************************************************************
3300
- * TODO con límite máximo o máximo height o con scroll
3301
- * en un gobj propio para gestionar los datos en "page"s
3302
- * que los hook no vengan rellenos si son muchos y que se puedan gestionar
3303
- * con un gobj
3304
- ***************************************************************************/
3305
- function show_dropdown_popup_menu(gobj, x, y, items, callback)
3306
- {
3307
- let $element = createElement2([
3308
- 'div', {class: 'dropdown popup' }, [
3309
- ['div', {
3310
- class: 'dropdown-menu', role: 'menu', style: 'min-width:4rem; border: 2px solid var(--bulma-border); padding: 0px;'
3311
- }, [
3312
- ['div', { class: 'dropdown-content', style: 'padding: 0;' }, []]
3313
- ]]
3314
- ], {
3315
- 'click': (evt) => {
3316
- evt.stopPropagation();
3317
- destroyModal();
3318
- if(callback) {
3319
- callback(evt);
3320
- }
3321
- }
3322
- }
3323
- ]);
3324
-
3325
- const destroyModal = () => {
3326
- $element.classList.remove('is-active');
3327
- $element.parentNode.removeChild($element);
3328
- };
3329
-
3330
- let $dropdown_content = $element.querySelector('.dropdown-content');
3331
-
3332
- let ids = kwid_get_ids(gobj, items);
3333
- for(let id of ids) {
3334
- let $item = createElement2(
3335
- ['a', {class: 'dropdown-item flex-horizontal-section', 'data-value':`${id}`, style:'margin:0px;'}, [
3336
- ['span', {i18n: `${id}`}, `${id}`]
3337
- ], {
3338
- 'click': (evt) => {
3339
- evt.stopPropagation();
3340
- destroyModal();
3341
- if(callback) {
3342
- callback(evt, this.dataset.value);
3343
- }
3344
- }
3345
- }]
3346
- );
3347
- $dropdown_content.appendChild($item);
3348
- }
3349
-
3350
- refresh_language($element, t);
3351
-
3352
- /*
3353
- * Add to popup layer
3354
- */
3355
- popup_mount_layer(gobj).appendChild($element);
3356
-
3357
- /*
3358
- * Set position
3359
- */
3360
- $element.style.position = "absolute";
3361
- $element.style.top = y + "px";
3362
- $element.style.left = x + "px";
3363
-
3364
- /*
3365
- * Show
3366
- */
3367
- $element.classList.add('is-active');
3368
-
3369
- /*
3370
- * Set focus
3371
- */
3372
- let $with_focus = $element.querySelector('.with-focus');
3373
- if($with_focus) {
3374
- $with_focus.focus();
3375
- }
3376
- }
3377
3518
 
3378
3519
 
3379
3520
 
@@ -4111,17 +4252,78 @@ function ac_change_locale(gobj, event, kw, src)
4111
4252
  ************************************************************/
4112
4253
  function ac_show_hook_data(gobj, event, kw, src)
4113
4254
  {
4114
- 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
+ }
4276
+
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
+ }
4115
4287
 
4116
- let row = kwid_find_one_record(gobj, webix.data, kw.row_id, null);
4117
- if(row) {
4118
- let cell = row[kw.col_id];
4119
- /*
4120
- * WARNING TODO hooks can have millions of kids
4121
- */
4122
- show_dropdown_popup_menu(gobj, kw.click_x, kw.click_y, cell);
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;
4123
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
+ }
4124
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);
4125
4327
  return 0;
4126
4328
  }
4127
4329
 
@@ -4473,20 +4675,12 @@ function ac_search(gobj, event, kw, src)
4473
4675
  return -1;
4474
4676
  }
4475
4677
 
4476
- let term = ((kw && kw.text) || "").toLowerCase();
4477
- if(!term) {
4478
- tabulator.clearFilter();
4479
- return 0;
4480
- }
4481
-
4482
- /* A treedb row is not flat: an fkey arrives as a list of objects, and
4483
- * `String()` of that is "[object Object]" -- so searching for the
4484
- * workshop of a meter, which is where the value a person has in mind
4485
- * lives, never found anything. `row_matches()` walks into them, and
4486
- * reads only the `id` of an fkey. See yui_row_search.js. */
4487
- tabulator.setFilter(function(data) {
4488
- return row_matches(data, term);
4489
- });
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);
4490
4684
 
4491
4685
  return 0;
4492
4686
  }
@@ -4720,6 +4914,9 @@ function create_gclass(gclass_name)
4720
4914
  ["EV_COPY_ROWS", ac_copy_rows, null],
4721
4915
  ["EV_PASTE_ROWS", ac_paste_rows, null],
4722
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],
4723
4920
  ["EV_SHOW_CELL_JSON", ac_show_cell_json, null],
4724
4921
  ["EV_SHOW_CELL_FILE", ac_show_cell_file, null],
4725
4922
  ["EV_ASSET_LOADED", ac_asset_loaded, null],
@@ -4764,7 +4961,11 @@ function create_gclass(gclass_name)
4764
4961
  ["EV_CLEAR_SELECTION", 0],
4765
4962
  ["EV_SELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
4766
4963
  ["EV_UNSELECT_ROWS", event_flag_t.EVF_OUTPUT_EVENT],
4767
- ["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],
4768
4969
  ["EV_SHOW_CELL_JSON", 0],
4769
4970
  ["EV_SHOW_CELL_FILE", 0],
4770
4971
  ["EV_ASSET_LOADED", 0],
@@ -2384,6 +2384,47 @@ function ac_request_page(gobj, event, kw, src)
2384
2384
  return 0;
2385
2385
  }
2386
2386
 
2387
+ /********************************************
2388
+ * Event from formtable: open the records a hook of one row links --
2389
+ * the CHILD topic's table, filtered to the rows whose fkey names that
2390
+ * row. A hook is the question "which records hang from this one", and
2391
+ * the child's table is where that answer can be read: it pages, it
2392
+ * searches, it opens a record and it exports -- which a list of ids in
2393
+ * a popup could not do past a few dozen of them.
2394
+ * {
2395
+ * topic_name: the child topic
2396
+ * fkey: its fkey column that points back
2397
+ * parent_topic: the topic of the row the hook is on
2398
+ * parent_id: that row's id
2399
+ * hook: the hook column
2400
+ * }
2401
+ ********************************************/
2402
+ function ac_open_linked(gobj, event, kw, src)
2403
+ {
2404
+ let child = get_gobj_formtable(gobj, kw.topic_name);
2405
+ if(!child) {
2406
+ /* A child topic this view does not show: a system topic in a
2407
+ * user view, or one the schema names and the backend did not
2408
+ * describe. */
2409
+ log_error(`${gobj_short_name(gobj)}: cannot open '${kw.topic_name}', ` +
2410
+ `linked from '${kw.parent_topic}^${kw.parent_id}': that topic is not in this view`);
2411
+ return -1;
2412
+ }
2413
+
2414
+ /* Through the same entry point as a tab click: the switch is local
2415
+ * and the host mirrors it into the URL, so Back returns to the
2416
+ * topic the hook was clicked in. */
2417
+ select_topic_by_id(gobj, gobj_name(child));
2418
+
2419
+ gobj_send_event(child, "EV_FILTER_BY_PARENT", {
2420
+ fkey: kw.fkey,
2421
+ parent_topic: kw.parent_topic,
2422
+ parent_id: kw.parent_id,
2423
+ hook: kw.hook
2424
+ }, gobj);
2425
+ return 0;
2426
+ }
2427
+
2387
2428
  /********************************************
2388
2429
  * Event from formtable: the bytes of one asset a `file` column names,
2389
2430
  * for the form's preview or the table's file popup.
@@ -2676,6 +2717,7 @@ function create_gclass(gclass_name)
2676
2717
  ["EV_UPDATE_FIELD", ac_update_field, null],
2677
2718
  ["EV_REQUEST_PAGE", ac_request_page, null],
2678
2719
  ["EV_REQUEST_ASSET", ac_request_asset, null],
2720
+ ["EV_OPEN_LINKED", ac_open_linked, null],
2679
2721
  ["EV_DELETE_RECORD", ac_delete_record, null],
2680
2722
  ["EV_REFRESH_TOPIC", ac_refresh_topic, null],
2681
2723
  ["EV_OPEN_JSON", ac_open_json, null],
@@ -2706,6 +2748,7 @@ function create_gclass(gclass_name)
2706
2748
  ["EV_UPDATE_FIELD", 0],
2707
2749
  ["EV_REQUEST_PAGE", 0],
2708
2750
  ["EV_REQUEST_ASSET", 0],
2751
+ ["EV_OPEN_LINKED", 0],
2709
2752
  ["EV_DELETE_RECORD", 0],
2710
2753
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT|
2711
2754
  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", () => {