@yuneta/gobj-ui 7.0.1 → 7.1.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.
package/README.md CHANGED
@@ -491,6 +491,37 @@ The buttons of that toolbar never shrink. When the row runs out of room, the
491
491
  url is what gives way, cut with an ellipsis, and the whole value stays in the
492
492
  `title` and the `aria-label`.
493
493
 
494
+ ### Reading a topic table: filters, columns, CSV
495
+
496
+ `C_YUI_TREEDB_TOPIC_WITH_FORM` had one global search box over the loaded rows.
497
+ Three tools join it, each behind its own flag, all **on** by default:
498
+
499
+ | attr | what it adds |
500
+ |---|---|
501
+ | `with_header_filters` | a filter box in the header of each column a match means something on |
502
+ | `with_columns_button` | a dialog that ticks which columns the table shows |
503
+ | `with_export_button` | downloads what the table holds as CSV |
504
+
505
+ **Not every column gets a filter box, on purpose.** A hook holds children, a
506
+ dict holds a subtree, and a date cell shows a formatted string over an epoch
507
+ number — a text match against the raw value there answers a question nobody
508
+ asked, so those columns get no box rather than a box that lies. A `boolean`
509
+ gets a tristate tick, an `enum` gets a list of its own values, and an `fkey`
510
+ gets a box whose match stringifies the value first, because *which rows point
511
+ at X* is the question fkey columns exist to answer and a fkey arrives as a ref
512
+ string, a list of them or a dict.
513
+
514
+ The search box and the header filters are **separate layers**: clearing the
515
+ search does not silently drop the column filters. The CSV carries what the
516
+ table HOLDS — the loaded rows, the visible columns, both filters applied, which
517
+ is what the reader is looking at. It is not the topic: a server-side dump of
518
+ every node is not something this view can stream, and the button's title says
519
+ so.
520
+
521
+ Searching is a user action, so it crosses the FSM (`EV_SEARCH`) like the rest;
522
+ it used to call `tabulator.setFilter` straight from the DOM handler, where the
523
+ `machine` trace could not see it.
524
+
494
525
  ### Read-only treedbs: `readonly`
495
526
 
496
527
  `C_YUI_TREEDB_TOPICS` and `C_YUI_TREEDB_GRAPH` take a **`readonly`** attr; the
@@ -11017,7 +11017,7 @@ function ac_expand_collapsed(gobj, event, kw, src) {
11017
11017
  /************************************************************
11018
11018
  * EV_SEARCH { text }
11019
11019
  ************************************************************/
11020
- function ac_search(gobj, event, kw, src) {
11020
+ function ac_search$1(gobj, event, kw, src) {
11021
11021
  let priv = gobj.priv;
11022
11022
  priv.search = (kw.text || "").trim().toLowerCase();
11023
11023
  render_tree(gobj);
@@ -11166,7 +11166,7 @@ function create_gclass$10(gclass_name) {
11166
11166
  ],
11167
11167
  [
11168
11168
  "EV_SEARCH",
11169
- ac_search,
11169
+ ac_search$1,
11170
11170
  null
11171
11171
  ],
11172
11172
  [
@@ -16222,6 +16222,9 @@ var attrs_table$5 = [
16222
16222
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_refresh_button", 0, true, "Button toolbar REFRESH"),
16223
16223
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_search_button", 0, true, "Button toolbar SEARCH"),
16224
16224
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_schema_button", 0, true, "Button toolbar SCHEMA (show the topic's desc)"),
16225
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button toolbar COLUMNS (choose which columns the table shows)"),
16226
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
16227
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_header_filters", 0, true, "Per-column filter box in the table header, on the columns a text/number match means something (not hooks, fkeys or json)"),
16225
16228
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
16226
16229
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16227
16230
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
@@ -16514,6 +16517,8 @@ function build_ui$5(gobj) {
16514
16517
  let with_refresh_button = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_refresh_button");
16515
16518
  let with_search_button = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_search_button");
16516
16519
  let with_schema_button = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_schema_button");
16520
+ let with_columns_button = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_columns_button");
16521
+ let with_export_button = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_export_button");
16517
16522
  if (with_search_button) {
16518
16523
  let search_id = `${toolbar_id}_search`;
16519
16524
  let $search_box = (0, _yuneta_gobj_js.createElement2)([
@@ -16534,17 +16539,7 @@ function build_ui$5(gobj) {
16534
16539
  let $search_input = $search_box.querySelector(`#${search_id}`);
16535
16540
  attach_clear($search_box, $search_input);
16536
16541
  if ($search_input) $search_input.addEventListener("input", (event) => {
16537
- let searchText = event.target.value.trim().toLowerCase();
16538
- let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
16539
- if (!tabulator) return;
16540
- if (searchText) tabulator.setFilter(function(data) {
16541
- return Object.entries(data).some(([key, val]) => {
16542
- if (key.startsWith("_")) return false;
16543
- if (val === null || val === void 0) return false;
16544
- return String(val).toLowerCase().includes(searchText);
16545
- });
16546
- });
16547
- else tabulator.clearFilter();
16542
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_SEARCH", { text: event.target.value.trim() }, gobj);
16548
16543
  });
16549
16544
  $view_toolbar.appendChild($search_box);
16550
16545
  }
@@ -16598,6 +16593,58 @@ function build_ui$5(gobj) {
16598
16593
  ]);
16599
16594
  $view_toolbar.appendChild($schema);
16600
16595
  }
16596
+ if (with_columns_button) {
16597
+ let $columns = (0, _yuneta_gobj_js.createElement2)([
16598
+ "button",
16599
+ {
16600
+ class: "TREEDB_TABLE_COLUMNS button mr-1",
16601
+ title: (0, i18next.t)("choose the columns to show"),
16602
+ "data-i18n-title": "choose the columns to show",
16603
+ "aria-label": (0, i18next.t)("columns"),
16604
+ "data-i18n-aria-label": "columns"
16605
+ },
16606
+ [["i", { class: "yi-table" }], [
16607
+ "span",
16608
+ {
16609
+ class: "is-hidden-mobile",
16610
+ i18n: "columns",
16611
+ style: "padding-left:5px;"
16612
+ },
16613
+ "columns"
16614
+ ]],
16615
+ { "click": (event) => {
16616
+ event.stopPropagation();
16617
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_OPEN_COLUMNS", {}, gobj);
16618
+ } }
16619
+ ]);
16620
+ $view_toolbar.appendChild($columns);
16621
+ }
16622
+ if (with_export_button) {
16623
+ let $export = (0, _yuneta_gobj_js.createElement2)([
16624
+ "button",
16625
+ {
16626
+ class: "TREEDB_TABLE_EXPORT button mr-1",
16627
+ title: (0, i18next.t)("download the rows loaded in this table as csv"),
16628
+ "data-i18n-title": "download the rows loaded in this table as csv",
16629
+ "aria-label": (0, i18next.t)("export"),
16630
+ "data-i18n-aria-label": "export"
16631
+ },
16632
+ [["i", { class: "yi-download" }], [
16633
+ "span",
16634
+ {
16635
+ class: "is-hidden-mobile",
16636
+ i18n: "export",
16637
+ style: "padding-left:5px;"
16638
+ },
16639
+ "export"
16640
+ ]],
16641
+ { "click": (event) => {
16642
+ event.stopPropagation();
16643
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_EXPORT_TABLE", {}, gobj);
16644
+ } }
16645
+ ]);
16646
+ $view_toolbar.appendChild($export);
16647
+ }
16601
16648
  let table_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "table_id");
16602
16649
  let $container = (0, _yuneta_gobj_js.createElement2)([
16603
16650
  "div",
@@ -16686,6 +16733,7 @@ function col_title_formatter(gobj, col) {
16686
16733
  function create_tabulator(gobj) {
16687
16734
  let table_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "table_id");
16688
16735
  let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
16736
+ let with_header_filters = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_header_filters");
16689
16737
  let columns = [];
16690
16738
  let with_checkbox = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_checkbox");
16691
16739
  let with_radio = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_radio");
@@ -16729,6 +16777,46 @@ function create_tabulator(gobj) {
16729
16777
  col_id: cell.getField()
16730
16778
  }, gobj);
16731
16779
  }
16780
+ const FILTERABLE = [
16781
+ "string",
16782
+ "integer",
16783
+ "real",
16784
+ "rowid",
16785
+ "uuid",
16786
+ "qualified",
16787
+ "email",
16788
+ "url",
16789
+ "tel",
16790
+ "id",
16791
+ "hex",
16792
+ "currency",
16793
+ "percent"
16794
+ ];
16795
+ function fkey_matches(term, value) {
16796
+ if (term === "" || term === null || term === void 0) return true;
16797
+ if (value === null || value === void 0) return false;
16798
+ return (typeof value === "string" ? value : JSON.stringify(value)).toLowerCase().includes(String(term).toLowerCase());
16799
+ }
16800
+ function apply_header_filter(colDef, field_desc) {
16801
+ switch (field_desc.type) {
16802
+ case "boolean":
16803
+ colDef.headerFilter = "tickCross";
16804
+ colDef.headerFilterParams = { tristate: true };
16805
+ break;
16806
+ case "enum":
16807
+ colDef.headerFilter = "list";
16808
+ colDef.headerFilterParams = {
16809
+ values: field_desc.enum_list || [],
16810
+ clearable: true
16811
+ };
16812
+ break;
16813
+ case "fkey":
16814
+ colDef.headerFilter = "input";
16815
+ colDef.headerFilterFunc = fkey_matches;
16816
+ break;
16817
+ default: if (FILTERABLE.includes(field_desc.type)) colDef.headerFilter = "input";
16818
+ }
16819
+ }
16732
16820
  for (let i = 0; i < desc.cols.length; i++) {
16733
16821
  let col = desc.cols[i];
16734
16822
  if (!col.id || col.id[0] === "_") continue;
@@ -16809,6 +16897,7 @@ function create_tabulator(gobj) {
16809
16897
  hozAlign,
16810
16898
  formatter: colFormatter
16811
16899
  };
16900
+ if (with_header_filters) apply_header_filter(colDef, field_desc);
16812
16901
  if (vertAlign) colDef.vertAlign = vertAlign;
16813
16902
  if (formatterParams) colDef.formatterParams = formatterParams;
16814
16903
  if (cellClick) colDef.cellClick = cellClick;
@@ -18018,6 +18107,125 @@ function ac_show_schema(gobj, event, kw, src) {
18018
18107
  return 0;
18019
18108
  }
18020
18109
  /************************************************************
18110
+ * Filter the loaded rows by a term matched against every non-internal
18111
+ * field. `clearFilter()` with no argument drops only THIS filter: the
18112
+ * per-column header filters are a separate layer and survive, so a
18113
+ * cleared search box does not silently undo them.
18114
+ ************************************************************/
18115
+ function ac_search(gobj, event, kw, src) {
18116
+ let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
18117
+ if (!tabulator) {
18118
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no table to search`);
18119
+ return -1;
18120
+ }
18121
+ let term = (kw && kw.text || "").toLowerCase();
18122
+ if (!term) {
18123
+ tabulator.clearFilter();
18124
+ return 0;
18125
+ }
18126
+ tabulator.setFilter(function(data) {
18127
+ return Object.entries(data).some(([key, val]) => {
18128
+ if (key.startsWith("_")) return false;
18129
+ if (val === null || val === void 0) return false;
18130
+ return String(val).toLowerCase().includes(term);
18131
+ });
18132
+ });
18133
+ return 0;
18134
+ }
18135
+ /************************************************************
18136
+ * Which columns the table shows: one checkbox per column, the current
18137
+ * visibility ticked. The internal columns (`_check_box_state_`,
18138
+ * `_operation`) are not offered — they are chrome, not data.
18139
+ ************************************************************/
18140
+ function ac_open_columns(gobj, event, kw, src) {
18141
+ let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
18142
+ if (!tabulator) {
18143
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no table, no columns to choose`);
18144
+ return -1;
18145
+ }
18146
+ let shell = yui_shell_of(gobj);
18147
+ if (!shell) {
18148
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no shell, cannot open the column chooser`);
18149
+ return -1;
18150
+ }
18151
+ let $list = (0, _yuneta_gobj_js.createElement2)(["div", { class: "TREEDB_COLUMNS_LIST" }]);
18152
+ let columns = tabulator.getColumns();
18153
+ for (let i = 0; i < columns.length; i++) {
18154
+ let column = columns[i];
18155
+ let field = column.getField();
18156
+ if (!field || field[0] === "_") continue;
18157
+ let $cb = (0, _yuneta_gobj_js.createElement2)(["input", {
18158
+ type: "checkbox",
18159
+ class: "mr-2"
18160
+ }]);
18161
+ $cb.checked = column.isVisible();
18162
+ $cb.addEventListener("change", () => {
18163
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_TOGGLE_COLUMN", {
18164
+ field,
18165
+ visible: $cb.checked
18166
+ }, gobj);
18167
+ });
18168
+ $list.appendChild((0, _yuneta_gobj_js.createElement2)([
18169
+ "label",
18170
+ { class: "checkbox is-block mb-1 TREEDB_COLUMN_ITEM" },
18171
+ [$cb, [
18172
+ "span",
18173
+ { class: "ml-2" },
18174
+ column.getDefinition().title || field
18175
+ ]]
18176
+ ]));
18177
+ }
18178
+ yui_shell_show_modal(shell, $list, {
18179
+ dialog: true,
18180
+ logical_class: "TREEDB_COLUMNS_DIALOG",
18181
+ title_prefix: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
18182
+ title: "columns",
18183
+ t: i18next.t
18184
+ });
18185
+ return 0;
18186
+ }
18187
+ /************************************************************
18188
+ * Show / hide one column.
18189
+ ************************************************************/
18190
+ function ac_toggle_column(gobj, event, kw, src) {
18191
+ let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
18192
+ let field = kw && kw.field || "";
18193
+ if (!tabulator || !field) {
18194
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no column '${field}' to toggle`);
18195
+ return -1;
18196
+ }
18197
+ try {
18198
+ if (kw.visible) tabulator.showColumn(field);
18199
+ else tabulator.hideColumn(field);
18200
+ } catch (e) {
18201
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot toggle column '${field}': ${e}`);
18202
+ return -1;
18203
+ }
18204
+ return 0;
18205
+ }
18206
+ /************************************************************
18207
+ * Download what the table HOLDS as CSV: the loaded rows, the visible
18208
+ * columns, search and header filters applied — which is what the reader
18209
+ * is looking at. Not the topic: a server-side dump of every node is not
18210
+ * something this view can stream.
18211
+ ************************************************************/
18212
+ function ac_export_table(gobj, event, kw, src) {
18213
+ let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
18214
+ if (!tabulator) {
18215
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no table to export`);
18216
+ return -1;
18217
+ }
18218
+ let name = `${(0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name")}-${(0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name")}.csv`;
18219
+ name = name.replace(/[^\w.\-]+/g, "_");
18220
+ try {
18221
+ tabulator.download("csv", name);
18222
+ } catch (e) {
18223
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: CSV export failed: ${e}`);
18224
+ return -1;
18225
+ }
18226
+ return 0;
18227
+ }
18228
+ /************************************************************
18021
18229
  * {
18022
18230
  * href: href
18023
18231
  * }
@@ -18141,6 +18349,26 @@ function create_gclass$5(gclass_name) {
18141
18349
  ac_show_schema,
18142
18350
  null
18143
18351
  ],
18352
+ [
18353
+ "EV_SEARCH",
18354
+ ac_search,
18355
+ null
18356
+ ],
18357
+ [
18358
+ "EV_OPEN_COLUMNS",
18359
+ ac_open_columns,
18360
+ null
18361
+ ],
18362
+ [
18363
+ "EV_TOGGLE_COLUMN",
18364
+ ac_toggle_column,
18365
+ null
18366
+ ],
18367
+ [
18368
+ "EV_EXPORT_TABLE",
18369
+ ac_export_table,
18370
+ null
18371
+ ],
18144
18372
  [
18145
18373
  "EV_SHOW",
18146
18374
  ac_show$4,
@@ -18171,6 +18399,10 @@ function create_gclass$5(gclass_name) {
18171
18399
  ["EV_CHANGE_LOCALE", 0],
18172
18400
  ["EV_REFRESH", 0],
18173
18401
  ["EV_SHOW_SCHEMA", 0],
18402
+ ["EV_SEARCH", 0],
18403
+ ["EV_OPEN_COLUMNS", 0],
18404
+ ["EV_TOGGLE_COLUMN", 0],
18405
+ ["EV_EXPORT_TABLE", 0],
18174
18406
  ["EV_CREATE_RECORD", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
18175
18407
  ["EV_UPDATE_RECORD", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
18176
18408
  ["EV_DELETE_RECORD", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
@@ -11012,7 +11012,7 @@ function ac_expand_collapsed(gobj, event, kw, src) {
11012
11012
  /************************************************************
11013
11013
  * EV_SEARCH { text }
11014
11014
  ************************************************************/
11015
- function ac_search(gobj, event, kw, src) {
11015
+ function ac_search$1(gobj, event, kw, src) {
11016
11016
  let priv = gobj.priv;
11017
11017
  priv.search = (kw.text || "").trim().toLowerCase();
11018
11018
  render_tree(gobj);
@@ -11161,7 +11161,7 @@ function create_gclass$10(gclass_name) {
11161
11161
  ],
11162
11162
  [
11163
11163
  "EV_SEARCH",
11164
- ac_search,
11164
+ ac_search$1,
11165
11165
  null
11166
11166
  ],
11167
11167
  [
@@ -16217,6 +16217,9 @@ var attrs_table$5 = [
16217
16217
  SDATA(data_type_t.DTP_BOOLEAN, "with_refresh_button", 0, true, "Button toolbar REFRESH"),
16218
16218
  SDATA(data_type_t.DTP_BOOLEAN, "with_search_button", 0, true, "Button toolbar SEARCH"),
16219
16219
  SDATA(data_type_t.DTP_BOOLEAN, "with_schema_button", 0, true, "Button toolbar SCHEMA (show the topic's desc)"),
16220
+ SDATA(data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button toolbar COLUMNS (choose which columns the table shows)"),
16221
+ SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
16222
+ SDATA(data_type_t.DTP_BOOLEAN, "with_header_filters", 0, true, "Per-column filter box in the table header, on the columns a text/number match means something (not hooks, fkeys or json)"),
16220
16223
  SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
16221
16224
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16222
16225
  SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
@@ -16509,6 +16512,8 @@ function build_ui$5(gobj) {
16509
16512
  let with_refresh_button = gobj_read_bool_attr(gobj, "with_refresh_button");
16510
16513
  let with_search_button = gobj_read_bool_attr(gobj, "with_search_button");
16511
16514
  let with_schema_button = gobj_read_bool_attr(gobj, "with_schema_button");
16515
+ let with_columns_button = gobj_read_bool_attr(gobj, "with_columns_button");
16516
+ let with_export_button = gobj_read_bool_attr(gobj, "with_export_button");
16512
16517
  if (with_search_button) {
16513
16518
  let search_id = `${toolbar_id}_search`;
16514
16519
  let $search_box = createElement2([
@@ -16529,17 +16534,7 @@ function build_ui$5(gobj) {
16529
16534
  let $search_input = $search_box.querySelector(`#${search_id}`);
16530
16535
  attach_clear($search_box, $search_input);
16531
16536
  if ($search_input) $search_input.addEventListener("input", (event) => {
16532
- let searchText = event.target.value.trim().toLowerCase();
16533
- let tabulator = gobj_read_attr(gobj, "tabulator");
16534
- if (!tabulator) return;
16535
- if (searchText) tabulator.setFilter(function(data) {
16536
- return Object.entries(data).some(([key, val]) => {
16537
- if (key.startsWith("_")) return false;
16538
- if (val === null || val === void 0) return false;
16539
- return String(val).toLowerCase().includes(searchText);
16540
- });
16541
- });
16542
- else tabulator.clearFilter();
16537
+ gobj_send_event(gobj, "EV_SEARCH", { text: event.target.value.trim() }, gobj);
16543
16538
  });
16544
16539
  $view_toolbar.appendChild($search_box);
16545
16540
  }
@@ -16593,6 +16588,58 @@ function build_ui$5(gobj) {
16593
16588
  ]);
16594
16589
  $view_toolbar.appendChild($schema);
16595
16590
  }
16591
+ if (with_columns_button) {
16592
+ let $columns = createElement2([
16593
+ "button",
16594
+ {
16595
+ class: "TREEDB_TABLE_COLUMNS button mr-1",
16596
+ title: t("choose the columns to show"),
16597
+ "data-i18n-title": "choose the columns to show",
16598
+ "aria-label": t("columns"),
16599
+ "data-i18n-aria-label": "columns"
16600
+ },
16601
+ [["i", { class: "yi-table" }], [
16602
+ "span",
16603
+ {
16604
+ class: "is-hidden-mobile",
16605
+ i18n: "columns",
16606
+ style: "padding-left:5px;"
16607
+ },
16608
+ "columns"
16609
+ ]],
16610
+ { "click": (event) => {
16611
+ event.stopPropagation();
16612
+ gobj_send_event(gobj, "EV_OPEN_COLUMNS", {}, gobj);
16613
+ } }
16614
+ ]);
16615
+ $view_toolbar.appendChild($columns);
16616
+ }
16617
+ if (with_export_button) {
16618
+ let $export = createElement2([
16619
+ "button",
16620
+ {
16621
+ class: "TREEDB_TABLE_EXPORT button mr-1",
16622
+ title: t("download the rows loaded in this table as csv"),
16623
+ "data-i18n-title": "download the rows loaded in this table as csv",
16624
+ "aria-label": t("export"),
16625
+ "data-i18n-aria-label": "export"
16626
+ },
16627
+ [["i", { class: "yi-download" }], [
16628
+ "span",
16629
+ {
16630
+ class: "is-hidden-mobile",
16631
+ i18n: "export",
16632
+ style: "padding-left:5px;"
16633
+ },
16634
+ "export"
16635
+ ]],
16636
+ { "click": (event) => {
16637
+ event.stopPropagation();
16638
+ gobj_send_event(gobj, "EV_EXPORT_TABLE", {}, gobj);
16639
+ } }
16640
+ ]);
16641
+ $view_toolbar.appendChild($export);
16642
+ }
16596
16643
  let table_id = gobj_read_str_attr(gobj, "table_id");
16597
16644
  let $container = createElement2([
16598
16645
  "div",
@@ -16681,6 +16728,7 @@ function col_title_formatter(gobj, col) {
16681
16728
  function create_tabulator(gobj) {
16682
16729
  let table_id = gobj_read_str_attr(gobj, "table_id");
16683
16730
  let desc = gobj_read_attr(gobj, "desc");
16731
+ let with_header_filters = gobj_read_bool_attr(gobj, "with_header_filters");
16684
16732
  let columns = [];
16685
16733
  let with_checkbox = gobj_read_bool_attr(gobj, "with_checkbox");
16686
16734
  let with_radio = gobj_read_bool_attr(gobj, "with_radio");
@@ -16724,6 +16772,46 @@ function create_tabulator(gobj) {
16724
16772
  col_id: cell.getField()
16725
16773
  }, gobj);
16726
16774
  }
16775
+ const FILTERABLE = [
16776
+ "string",
16777
+ "integer",
16778
+ "real",
16779
+ "rowid",
16780
+ "uuid",
16781
+ "qualified",
16782
+ "email",
16783
+ "url",
16784
+ "tel",
16785
+ "id",
16786
+ "hex",
16787
+ "currency",
16788
+ "percent"
16789
+ ];
16790
+ function fkey_matches(term, value) {
16791
+ if (term === "" || term === null || term === void 0) return true;
16792
+ if (value === null || value === void 0) return false;
16793
+ return (typeof value === "string" ? value : JSON.stringify(value)).toLowerCase().includes(String(term).toLowerCase());
16794
+ }
16795
+ function apply_header_filter(colDef, field_desc) {
16796
+ switch (field_desc.type) {
16797
+ case "boolean":
16798
+ colDef.headerFilter = "tickCross";
16799
+ colDef.headerFilterParams = { tristate: true };
16800
+ break;
16801
+ case "enum":
16802
+ colDef.headerFilter = "list";
16803
+ colDef.headerFilterParams = {
16804
+ values: field_desc.enum_list || [],
16805
+ clearable: true
16806
+ };
16807
+ break;
16808
+ case "fkey":
16809
+ colDef.headerFilter = "input";
16810
+ colDef.headerFilterFunc = fkey_matches;
16811
+ break;
16812
+ default: if (FILTERABLE.includes(field_desc.type)) colDef.headerFilter = "input";
16813
+ }
16814
+ }
16727
16815
  for (let i = 0; i < desc.cols.length; i++) {
16728
16816
  let col = desc.cols[i];
16729
16817
  if (!col.id || col.id[0] === "_") continue;
@@ -16804,6 +16892,7 @@ function create_tabulator(gobj) {
16804
16892
  hozAlign,
16805
16893
  formatter: colFormatter
16806
16894
  };
16895
+ if (with_header_filters) apply_header_filter(colDef, field_desc);
16807
16896
  if (vertAlign) colDef.vertAlign = vertAlign;
16808
16897
  if (formatterParams) colDef.formatterParams = formatterParams;
16809
16898
  if (cellClick) colDef.cellClick = cellClick;
@@ -18013,6 +18102,125 @@ function ac_show_schema(gobj, event, kw, src) {
18013
18102
  return 0;
18014
18103
  }
18015
18104
  /************************************************************
18105
+ * Filter the loaded rows by a term matched against every non-internal
18106
+ * field. `clearFilter()` with no argument drops only THIS filter: the
18107
+ * per-column header filters are a separate layer and survive, so a
18108
+ * cleared search box does not silently undo them.
18109
+ ************************************************************/
18110
+ function ac_search(gobj, event, kw, src) {
18111
+ let tabulator = gobj_read_attr(gobj, "tabulator");
18112
+ if (!tabulator) {
18113
+ log_error(`${gobj_short_name(gobj)}: no table to search`);
18114
+ return -1;
18115
+ }
18116
+ let term = (kw && kw.text || "").toLowerCase();
18117
+ if (!term) {
18118
+ tabulator.clearFilter();
18119
+ return 0;
18120
+ }
18121
+ tabulator.setFilter(function(data) {
18122
+ return Object.entries(data).some(([key, val]) => {
18123
+ if (key.startsWith("_")) return false;
18124
+ if (val === null || val === void 0) return false;
18125
+ return String(val).toLowerCase().includes(term);
18126
+ });
18127
+ });
18128
+ return 0;
18129
+ }
18130
+ /************************************************************
18131
+ * Which columns the table shows: one checkbox per column, the current
18132
+ * visibility ticked. The internal columns (`_check_box_state_`,
18133
+ * `_operation`) are not offered — they are chrome, not data.
18134
+ ************************************************************/
18135
+ function ac_open_columns(gobj, event, kw, src) {
18136
+ let tabulator = gobj_read_attr(gobj, "tabulator");
18137
+ if (!tabulator) {
18138
+ log_error(`${gobj_short_name(gobj)}: no table, no columns to choose`);
18139
+ return -1;
18140
+ }
18141
+ let shell = yui_shell_of(gobj);
18142
+ if (!shell) {
18143
+ log_error(`${gobj_short_name(gobj)}: no shell, cannot open the column chooser`);
18144
+ return -1;
18145
+ }
18146
+ let $list = createElement2(["div", { class: "TREEDB_COLUMNS_LIST" }]);
18147
+ let columns = tabulator.getColumns();
18148
+ for (let i = 0; i < columns.length; i++) {
18149
+ let column = columns[i];
18150
+ let field = column.getField();
18151
+ if (!field || field[0] === "_") continue;
18152
+ let $cb = createElement2(["input", {
18153
+ type: "checkbox",
18154
+ class: "mr-2"
18155
+ }]);
18156
+ $cb.checked = column.isVisible();
18157
+ $cb.addEventListener("change", () => {
18158
+ gobj_send_event(gobj, "EV_TOGGLE_COLUMN", {
18159
+ field,
18160
+ visible: $cb.checked
18161
+ }, gobj);
18162
+ });
18163
+ $list.appendChild(createElement2([
18164
+ "label",
18165
+ { class: "checkbox is-block mb-1 TREEDB_COLUMN_ITEM" },
18166
+ [$cb, [
18167
+ "span",
18168
+ { class: "ml-2" },
18169
+ column.getDefinition().title || field
18170
+ ]]
18171
+ ]));
18172
+ }
18173
+ yui_shell_show_modal(shell, $list, {
18174
+ dialog: true,
18175
+ logical_class: "TREEDB_COLUMNS_DIALOG",
18176
+ title_prefix: gobj_read_str_attr(gobj, "topic_name"),
18177
+ title: "columns",
18178
+ t
18179
+ });
18180
+ return 0;
18181
+ }
18182
+ /************************************************************
18183
+ * Show / hide one column.
18184
+ ************************************************************/
18185
+ function ac_toggle_column(gobj, event, kw, src) {
18186
+ let tabulator = gobj_read_attr(gobj, "tabulator");
18187
+ let field = kw && kw.field || "";
18188
+ if (!tabulator || !field) {
18189
+ log_error(`${gobj_short_name(gobj)}: no column '${field}' to toggle`);
18190
+ return -1;
18191
+ }
18192
+ try {
18193
+ if (kw.visible) tabulator.showColumn(field);
18194
+ else tabulator.hideColumn(field);
18195
+ } catch (e) {
18196
+ log_error(`${gobj_short_name(gobj)}: cannot toggle column '${field}': ${e}`);
18197
+ return -1;
18198
+ }
18199
+ return 0;
18200
+ }
18201
+ /************************************************************
18202
+ * Download what the table HOLDS as CSV: the loaded rows, the visible
18203
+ * columns, search and header filters applied — which is what the reader
18204
+ * is looking at. Not the topic: a server-side dump of every node is not
18205
+ * something this view can stream.
18206
+ ************************************************************/
18207
+ function ac_export_table(gobj, event, kw, src) {
18208
+ let tabulator = gobj_read_attr(gobj, "tabulator");
18209
+ if (!tabulator) {
18210
+ log_error(`${gobj_short_name(gobj)}: no table to export`);
18211
+ return -1;
18212
+ }
18213
+ let name = `${gobj_read_str_attr(gobj, "treedb_name")}-${gobj_read_str_attr(gobj, "topic_name")}.csv`;
18214
+ name = name.replace(/[^\w.\-]+/g, "_");
18215
+ try {
18216
+ tabulator.download("csv", name);
18217
+ } catch (e) {
18218
+ log_error(`${gobj_short_name(gobj)}: CSV export failed: ${e}`);
18219
+ return -1;
18220
+ }
18221
+ return 0;
18222
+ }
18223
+ /************************************************************
18016
18224
  * {
18017
18225
  * href: href
18018
18226
  * }
@@ -18136,6 +18344,26 @@ function create_gclass$5(gclass_name) {
18136
18344
  ac_show_schema,
18137
18345
  null
18138
18346
  ],
18347
+ [
18348
+ "EV_SEARCH",
18349
+ ac_search,
18350
+ null
18351
+ ],
18352
+ [
18353
+ "EV_OPEN_COLUMNS",
18354
+ ac_open_columns,
18355
+ null
18356
+ ],
18357
+ [
18358
+ "EV_TOGGLE_COLUMN",
18359
+ ac_toggle_column,
18360
+ null
18361
+ ],
18362
+ [
18363
+ "EV_EXPORT_TABLE",
18364
+ ac_export_table,
18365
+ null
18366
+ ],
18139
18367
  [
18140
18368
  "EV_SHOW",
18141
18369
  ac_show$4,
@@ -18166,6 +18394,10 @@ function create_gclass$5(gclass_name) {
18166
18394
  ["EV_CHANGE_LOCALE", 0],
18167
18395
  ["EV_REFRESH", 0],
18168
18396
  ["EV_SHOW_SCHEMA", 0],
18397
+ ["EV_SEARCH", 0],
18398
+ ["EV_OPEN_COLUMNS", 0],
18399
+ ["EV_TOGGLE_COLUMN", 0],
18400
+ ["EV_EXPORT_TABLE", 0],
18169
18401
  ["EV_CREATE_RECORD", event_flag_t.EVF_OUTPUT_EVENT],
18170
18402
  ["EV_UPDATE_RECORD", event_flag_t.EVF_OUTPUT_EVENT],
18171
18403
  ["EV_DELETE_RECORD", event_flag_t.EVF_OUTPUT_EVENT],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.0.1",
3
+ "version": "7.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -20,7 +20,7 @@
20
20
  "src/",
21
21
  "vite-plugin-yuneta-html.js"
22
22
  ],
23
- "description": "Yuneta UI Library \u2014 the active line: declarative shell (C_YUI_SHELL+NAV+PAGER+WIZARD) + windows, TreeDB views, forms, charts and maps. Consumed by wattyzer and the yunetas JS yunos (gui_agent, gui_treedb). The frozen legacy GClass GUI stack (C_YUI_MAIN/TABS/ROUTING) lives on the v1 branch, npm dist-tag 'legacy' (consumed by estadodelaire and hidraulia).",
23
+ "description": "Yuneta UI Library the active line: declarative shell (C_YUI_SHELL+NAV+PAGER+WIZARD) + windows, TreeDB views, forms, charts and maps. Consumed by wattyzer and the yunetas JS yunos (gui_agent, gui_treedb). The frozen legacy GClass GUI stack (C_YUI_MAIN/TABS/ROUTING) lives on the v1 branch, npm dist-tag 'legacy' (consumed by estadodelaire and hidraulia).",
24
24
  "scripts": {
25
25
  "build": "vite build",
26
26
  "test": "vitest run",
@@ -108,6 +108,9 @@ SDATA(data_type_t.DTP_BOOLEAN, "with_paste_button", 0, true, "Button
108
108
  SDATA(data_type_t.DTP_BOOLEAN, "with_refresh_button", 0, true, "Button toolbar REFRESH"),
109
109
  SDATA(data_type_t.DTP_BOOLEAN, "with_search_button", 0, true, "Button toolbar SEARCH"),
110
110
  SDATA(data_type_t.DTP_BOOLEAN, "with_schema_button", 0, true, "Button toolbar SCHEMA (show the topic's desc)"),
111
+ SDATA(data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button toolbar COLUMNS (choose which columns the table shows)"),
112
+ SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
113
+ SDATA(data_type_t.DTP_BOOLEAN, "with_header_filters", 0, true, "Per-column filter box in the table header, on the columns a text/number match means something (not hooks, fkeys or json)"),
111
114
  SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
112
115
 
113
116
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
@@ -496,6 +499,8 @@ function build_ui(gobj)
496
499
  let with_refresh_button = gobj_read_bool_attr(gobj, "with_refresh_button");
497
500
  let with_search_button = gobj_read_bool_attr(gobj, "with_search_button");
498
501
  let with_schema_button = gobj_read_bool_attr(gobj, "with_schema_button");
502
+ let with_columns_button = gobj_read_bool_attr(gobj, "with_columns_button");
503
+ let with_export_button = gobj_read_bool_attr(gobj, "with_export_button");
499
504
 
500
505
  if(with_search_button) {
501
506
  let search_id = `${toolbar_id}_search`;
@@ -521,27 +526,16 @@ function build_ui(gobj)
521
526
  * filter below re-runs (empty term → clearFilter). */
522
527
  attach_clear($search_box, $search_input);
523
528
  if($search_input) {
529
+ /* The DOM handler's only job is to turn the keystroke into an
530
+ * event: searching is a user action, so it crosses the FSM and
531
+ * shows up in the `machine` trace like every other one. */
524
532
  $search_input.addEventListener('input', (event) => {
525
- let searchText = event.target.value.trim().toLowerCase();
526
- let tabulator = gobj_read_attr(gobj, "tabulator");
527
- if(!tabulator) {
528
- return;
529
- }
530
- if(searchText) {
531
- tabulator.setFilter(function(data) {
532
- return Object.entries(data).some(([key, val]) => {
533
- if(key.startsWith('_')) {
534
- return false;
535
- }
536
- if(val === null || val === undefined) {
537
- return false;
538
- }
539
- return String(val).toLowerCase().includes(searchText);
540
- });
541
- });
542
- } else {
543
- tabulator.clearFilter();
544
- }
533
+ gobj_send_event(
534
+ gobj,
535
+ "EV_SEARCH",
536
+ {text: event.target.value.trim()},
537
+ gobj
538
+ );
545
539
  });
546
540
  }
547
541
  $view_toolbar.appendChild($search_box);
@@ -583,6 +577,48 @@ function build_ui(gobj)
583
577
  $view_toolbar.appendChild($schema);
584
578
  }
585
579
 
580
+ if(with_columns_button) {
581
+ /* A topic with a dozen columns is wider than the screen, and until
582
+ * now the reader could not decide WHICH of them to keep: the table
583
+ * simply scrolled sideways for ever. */
584
+ let $columns = createElement2(
585
+ ['button', {class: 'TREEDB_TABLE_COLUMNS button mr-1',
586
+ title: t('choose the columns to show'),
587
+ 'data-i18n-title': 'choose the columns to show',
588
+ 'aria-label': t('columns'), 'data-i18n-aria-label': 'columns'}, [
589
+ ['i', {class: 'yi-table'}],
590
+ ['span', {class: 'is-hidden-mobile', i18n: 'columns', style: 'padding-left:5px;'}, 'columns']
591
+ ], {
592
+ 'click': (event) => {
593
+ event.stopPropagation();
594
+ gobj_send_event(gobj, "EV_OPEN_COLUMNS", {}, gobj);
595
+ }
596
+ }]
597
+ );
598
+ $view_toolbar.appendChild($columns);
599
+ }
600
+
601
+ if(with_export_button) {
602
+ /* What the TABLE holds, filters and column choice applied — not the
603
+ * topic. A server-side dump of every node is not something this view
604
+ * can stream, and the title says so. */
605
+ let $export = createElement2(
606
+ ['button', {class: 'TREEDB_TABLE_EXPORT button mr-1',
607
+ title: t('download the rows loaded in this table as csv'),
608
+ 'data-i18n-title': 'download the rows loaded in this table as csv',
609
+ 'aria-label': t('export'), 'data-i18n-aria-label': 'export'}, [
610
+ ['i', {class: 'yi-download'}],
611
+ ['span', {class: 'is-hidden-mobile', i18n: 'export', style: 'padding-left:5px;'}, 'export']
612
+ ], {
613
+ 'click': (event) => {
614
+ event.stopPropagation();
615
+ gobj_send_event(gobj, "EV_EXPORT_TABLE", {}, gobj);
616
+ }
617
+ }]
618
+ );
619
+ $view_toolbar.appendChild($export);
620
+ }
621
+
586
622
  /*----------------------------------------------*
587
623
  * Layout Schema
588
624
  *----------------------------------------------*/
@@ -693,6 +729,7 @@ function create_tabulator(gobj)
693
729
  {
694
730
  let table_id = gobj_read_str_attr(gobj, "table_id");
695
731
  let desc = gobj_read_attr(gobj, "desc");
732
+ let with_header_filters = gobj_read_bool_attr(gobj, "with_header_filters");
696
733
 
697
734
  let columns = [];
698
735
 
@@ -773,6 +810,58 @@ function create_tabulator(gobj)
773
810
  );
774
811
  }
775
812
 
813
+ /* A filter box per column, on the columns where a text/number match
814
+ * MEANS something. A hook holds children, a dict holds a subtree and a
815
+ * date cell shows a formatted string over an epoch number — matching
816
+ * the raw value there answers a question nobody asked, so those columns
817
+ * get no box rather than a box that lies. An fkey does get one: "which
818
+ * rows point at X" is the question fkey columns exist to answer, and its
819
+ * value is stringified first because a fkey can arrive as a ref string,
820
+ * a list of them or a dict.
821
+ */
822
+ const FILTERABLE = [
823
+ "string", "integer", "real", "rowid", "uuid", "qualified",
824
+ "email", "url", "tel", "id", "hex", "currency", "percent"
825
+ ];
826
+
827
+ function fkey_matches(term, value)
828
+ {
829
+ if(term === "" || term === null || term === undefined) {
830
+ return true;
831
+ }
832
+ if(value === null || value === undefined) {
833
+ return false;
834
+ }
835
+ let text = (typeof value === "string")? value : JSON.stringify(value);
836
+ return text.toLowerCase().includes(String(term).toLowerCase());
837
+ }
838
+
839
+ function apply_header_filter(colDef, field_desc)
840
+ {
841
+ switch(field_desc.type) {
842
+ case "boolean":
843
+ colDef.headerFilter = "tickCross";
844
+ colDef.headerFilterParams = {tristate: true};
845
+ break;
846
+ case "enum":
847
+ colDef.headerFilter = "list";
848
+ colDef.headerFilterParams = {
849
+ values: field_desc.enum_list || [],
850
+ clearable: true
851
+ };
852
+ break;
853
+ case "fkey":
854
+ colDef.headerFilter = "input";
855
+ colDef.headerFilterFunc = fkey_matches;
856
+ break;
857
+ default:
858
+ if(FILTERABLE.includes(field_desc.type)) {
859
+ colDef.headerFilter = "input";
860
+ }
861
+ break;
862
+ }
863
+ }
864
+
776
865
  for (let i = 0; i < desc.cols.length; i++) {
777
866
  let col = desc.cols[i];
778
867
  if(!col.id || col.id[0]==='_') {
@@ -866,6 +955,9 @@ function create_tabulator(gobj)
866
955
  hozAlign: hozAlign,
867
956
  formatter: colFormatter,
868
957
  };
958
+ if(with_header_filters) {
959
+ apply_header_filter(colDef, field_desc);
960
+ }
869
961
  if(vertAlign) {
870
962
  colDef.vertAlign = vertAlign;
871
963
  }
@@ -2719,6 +2811,151 @@ function ac_show_schema(gobj, event, kw, src)
2719
2811
  return 0;
2720
2812
  }
2721
2813
 
2814
+ /************************************************************
2815
+ * Filter the loaded rows by a term matched against every non-internal
2816
+ * field. `clearFilter()` with no argument drops only THIS filter: the
2817
+ * per-column header filters are a separate layer and survive, so a
2818
+ * cleared search box does not silently undo them.
2819
+ ************************************************************/
2820
+ function ac_search(gobj, event, kw, src)
2821
+ {
2822
+ let tabulator = gobj_read_attr(gobj, "tabulator");
2823
+ if(!tabulator) {
2824
+ log_error(`${gobj_short_name(gobj)}: no table to search`);
2825
+ return -1;
2826
+ }
2827
+
2828
+ let term = ((kw && kw.text) || "").toLowerCase();
2829
+ if(!term) {
2830
+ tabulator.clearFilter();
2831
+ return 0;
2832
+ }
2833
+
2834
+ tabulator.setFilter(function(data) {
2835
+ return Object.entries(data).some(([key, val]) => {
2836
+ if(key.startsWith('_')) {
2837
+ return false;
2838
+ }
2839
+ if(val === null || val === undefined) {
2840
+ return false;
2841
+ }
2842
+ return String(val).toLowerCase().includes(term);
2843
+ });
2844
+ });
2845
+
2846
+ return 0;
2847
+ }
2848
+
2849
+ /************************************************************
2850
+ * Which columns the table shows: one checkbox per column, the current
2851
+ * visibility ticked. The internal columns (`_check_box_state_`,
2852
+ * `_operation`) are not offered — they are chrome, not data.
2853
+ ************************************************************/
2854
+ function ac_open_columns(gobj, event, kw, src)
2855
+ {
2856
+ let tabulator = gobj_read_attr(gobj, "tabulator");
2857
+ if(!tabulator) {
2858
+ log_error(`${gobj_short_name(gobj)}: no table, no columns to choose`);
2859
+ return -1;
2860
+ }
2861
+
2862
+ let shell = yui_shell_of(gobj);
2863
+ if(!shell) {
2864
+ log_error(`${gobj_short_name(gobj)}: no shell, cannot open the column chooser`);
2865
+ return -1;
2866
+ }
2867
+
2868
+ let $list = createElement2(['div', {class: 'TREEDB_COLUMNS_LIST'}]);
2869
+ let columns = tabulator.getColumns();
2870
+ for(let i = 0; i < columns.length; i++) {
2871
+ let column = columns[i];
2872
+ let field = column.getField();
2873
+ if(!field || field[0] === '_') {
2874
+ continue;
2875
+ }
2876
+ let $cb = createElement2(['input', {type: 'checkbox', class: 'mr-2'}]);
2877
+ $cb.checked = column.isVisible();
2878
+ $cb.addEventListener('change', () => {
2879
+ gobj_send_event(
2880
+ gobj,
2881
+ "EV_TOGGLE_COLUMN",
2882
+ {field: field, visible: $cb.checked},
2883
+ gobj
2884
+ );
2885
+ });
2886
+ $list.appendChild(createElement2(
2887
+ ['label', {class: 'checkbox is-block mb-1 TREEDB_COLUMN_ITEM'}, [
2888
+ $cb,
2889
+ ['span', {class: 'ml-2'}, column.getDefinition().title || field]
2890
+ ]]
2891
+ ));
2892
+ }
2893
+
2894
+ yui_shell_show_modal(shell, $list, {
2895
+ dialog: true,
2896
+ logical_class: "TREEDB_COLUMNS_DIALOG",
2897
+ title_prefix: gobj_read_str_attr(gobj, "topic_name"),
2898
+ title: "columns",
2899
+ t: t
2900
+ });
2901
+
2902
+ return 0;
2903
+ }
2904
+
2905
+ /************************************************************
2906
+ * Show / hide one column.
2907
+ ************************************************************/
2908
+ function ac_toggle_column(gobj, event, kw, src)
2909
+ {
2910
+ let tabulator = gobj_read_attr(gobj, "tabulator");
2911
+ let field = (kw && kw.field) || "";
2912
+ if(!tabulator || !field) {
2913
+ log_error(`${gobj_short_name(gobj)}: no column '${field}' to toggle`);
2914
+ return -1;
2915
+ }
2916
+
2917
+ try {
2918
+ if(kw.visible) {
2919
+ tabulator.showColumn(field);
2920
+ } else {
2921
+ tabulator.hideColumn(field);
2922
+ }
2923
+ } catch(e) {
2924
+ log_error(`${gobj_short_name(gobj)}: cannot toggle column '${field}': ${e}`);
2925
+ return -1;
2926
+ }
2927
+
2928
+ return 0;
2929
+ }
2930
+
2931
+ /************************************************************
2932
+ * Download what the table HOLDS as CSV: the loaded rows, the visible
2933
+ * columns, search and header filters applied — which is what the reader
2934
+ * is looking at. Not the topic: a server-side dump of every node is not
2935
+ * something this view can stream.
2936
+ ************************************************************/
2937
+ function ac_export_table(gobj, event, kw, src)
2938
+ {
2939
+ let tabulator = gobj_read_attr(gobj, "tabulator");
2940
+ if(!tabulator) {
2941
+ log_error(`${gobj_short_name(gobj)}: no table to export`);
2942
+ return -1;
2943
+ }
2944
+
2945
+ let name = `${gobj_read_str_attr(gobj, "treedb_name")}-` +
2946
+ `${gobj_read_str_attr(gobj, "topic_name")}.csv`;
2947
+ name = name.replace(/[^\w.\-]+/g, "_");
2948
+
2949
+ try {
2950
+ tabulator.download("csv", name);
2951
+ } catch(e) {
2952
+ log_error(`${gobj_short_name(gobj)}: CSV export failed: ${e}`);
2953
+ return -1;
2954
+ }
2955
+
2956
+ return 0;
2957
+ }
2958
+
2722
2959
  /************************************************************
2723
2960
  * {
2724
2961
  * href: href
@@ -2795,6 +3032,10 @@ function create_gclass(gclass_name)
2795
3032
  ["EV_CHANGE_LOCALE", ac_change_locale, null],
2796
3033
  ["EV_REFRESH", ac_refresh, null],
2797
3034
  ["EV_SHOW_SCHEMA", ac_show_schema, null],
3035
+ ["EV_SEARCH", ac_search, null],
3036
+ ["EV_OPEN_COLUMNS", ac_open_columns, null],
3037
+ ["EV_TOGGLE_COLUMN", ac_toggle_column, null],
3038
+ ["EV_EXPORT_TABLE", ac_export_table, null],
2798
3039
  ["EV_SHOW", ac_show, null],
2799
3040
  ["EV_HIDE", ac_hide, null]
2800
3041
  ]]
@@ -2823,6 +3064,10 @@ function create_gclass(gclass_name)
2823
3064
  ["EV_CHANGE_LOCALE", 0],
2824
3065
  ["EV_REFRESH", 0],
2825
3066
  ["EV_SHOW_SCHEMA", 0],
3067
+ ["EV_SEARCH", 0],
3068
+ ["EV_OPEN_COLUMNS", 0],
3069
+ ["EV_TOGGLE_COLUMN", 0],
3070
+ ["EV_EXPORT_TABLE", 0],
2826
3071
 
2827
3072
  ["EV_CREATE_RECORD", event_flag_t.EVF_OUTPUT_EVENT],
2828
3073
  ["EV_UPDATE_RECORD", event_flag_t.EVF_OUTPUT_EVENT],