@yuneta/gobj-ui 7.8.1 → 7.9.1

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.
@@ -14497,6 +14497,7 @@ var attrs_table$6 = [
14497
14497
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote yuno for data fetching"),
14498
14498
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
14499
14499
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
14500
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Each topic table pulls its rows a PAGE at a time (`nodes` from/limit) instead of loading the topic whole. Safe against a backend that cannot page: it answers the whole list, which the table reads as one page"),
14500
14501
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
14501
14502
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
14502
14503
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_cards_landing", 0, false, "Land on a grid of topic cards (list->detail): a card opens its table, with the tabs bar + a back-to-grid button. Off = tabs only (legacy)."),
@@ -15310,7 +15311,8 @@ function process_treedb_descs$1(gobj) {
15310
15311
  with_delete_button: !readonly,
15311
15312
  treedb_name,
15312
15313
  topic_name: key,
15313
- desc
15314
+ desc,
15315
+ with_remote_paging: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_remote_paging")
15314
15316
  };
15315
15317
  let id = `${(0, _yuneta_gobj_js.gobj_name)(gobj)}?${desc.topic_name}`;
15316
15318
  let gobj_topic_form = (0, _yuneta_gobj_js.gobj_create_service)(id, "C_YUI_TREEDB_TOPIC_WITH_FORM", kw_topic_form, gobj);
@@ -15381,6 +15383,8 @@ function register_sub_routes$1(gobj) {
15381
15383
  function get_nodes$1(gobj, topic_name) {
15382
15384
  const treedb_name = gobj.priv.treedb_name;
15383
15385
  subscribe_treedb$1(gobj, topic_name);
15386
+ let gobj_topic_form = get_gobj_formtable(gobj, topic_name);
15387
+ if (gobj_topic_form && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj_topic_form, "with_remote_paging")) return;
15384
15388
  treedb_nodes$1(gobj, treedb_name, topic_name, { list_dict: true });
15385
15389
  }
15386
15390
  /************************************************************
@@ -15428,7 +15432,7 @@ function get_gobj_formtable(gobj, topic_name) {
15428
15432
  /************************************************************
15429
15433
  * Command to remote service
15430
15434
  ************************************************************/
15431
- function treedb_nodes$1(gobj, treedb_name, topic_name, options) {
15435
+ function treedb_nodes$1(gobj, treedb_name, topic_name, options, page) {
15432
15436
  let command = "nodes";
15433
15437
  let gobj_remote_yuno = (0, _yuneta_gobj_js.gobj_read_pointer_attr)(gobj, "gobj_remote_yuno");
15434
15438
  let kw = {
@@ -15438,8 +15442,17 @@ function treedb_nodes$1(gobj, treedb_name, topic_name, options) {
15438
15442
  options: options || {}
15439
15443
  };
15440
15444
  kw.__md_command__ = { topic_name };
15445
+ if (page) {
15446
+ kw.from = page.from;
15447
+ kw.limit = page.limit;
15448
+ kw.__md_command__.req_id = page.req_id;
15449
+ }
15441
15450
  let ret = (0, _yuneta_gobj_js.gobj_command)(gobj_remote_yuno, command, kw, gobj);
15442
- if (ret) (0, _yuneta_gobj_js.log_error)(ret);
15451
+ if (ret) {
15452
+ (0, _yuneta_gobj_js.log_error)(ret);
15453
+ return -1;
15454
+ }
15455
+ return 0;
15443
15456
  }
15444
15457
  /************************************************************
15445
15458
  * Command to remote service
@@ -15673,7 +15686,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
15673
15686
  yui_shell_show_error(yui_shell_of(gobj), comment, { t: i18next.t });
15674
15687
  if (command === "update-node" || command === "create-node") {
15675
15688
  let failed_topic = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0);
15676
- if (failed_topic) treedb_nodes$1(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name"), failed_topic, { list_dict: true });
15689
+ if (failed_topic) (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
15677
15690
  }
15678
15691
  }
15679
15692
  return 0;
@@ -15689,7 +15702,15 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
15689
15702
  let topic_name = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", _yuneta_gobj_js.kw_flag_t.KW_REQUIRED);
15690
15703
  if (result >= 0) {
15691
15704
  let gobj_topic_form = (0, _yuneta_gobj_js.gobj_find_child)(gobj, { __gobj_name__: `${(0, _yuneta_gobj_js.gobj_name)(gobj)}?${topic_name}` });
15692
- (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_LOAD_NODES", nodes_answer(data).rows, gobj);
15705
+ let answer = nodes_answer(data);
15706
+ let req_id = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "req_id", "", 0);
15707
+ if (req_id) (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_PAGE_LOADED", {
15708
+ req_id,
15709
+ rows: answer.rows,
15710
+ pages: answer.pages,
15711
+ total: answer.total
15712
+ }, gobj);
15713
+ else (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_LOAD_NODES", answer.rows, gobj);
15693
15714
  }
15694
15715
  break;
15695
15716
  case "create-node":
@@ -15915,6 +15936,31 @@ function ac_create_record(gobj, event, kw, src) {
15915
15936
  });
15916
15937
  }
15917
15938
  /********************************************
15939
+ * A topic table wants a page.
15940
+ *
15941
+ * The transport is ours, so the table asks and we fetch; the answer
15942
+ * finds its way back by the `req_id` we echo. A backend that cannot
15943
+ * page answers the whole list, which the table takes as one page —
15944
+ * which is the truth, and is why the table can ask without knowing
15945
+ * what it is talking to.
15946
+ ********************************************/
15947
+ function ac_request_page(gobj, event, kw, src) {
15948
+ let topic_name = kw.topic_name || (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
15949
+ if (!topic_name || !kw.req_id) {
15950
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: a page request with no topic or no id`);
15951
+ return -1;
15952
+ }
15953
+ if (treedb_nodes$1(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name"), topic_name, { list_dict: true }, {
15954
+ from: kw.from,
15955
+ limit: kw.limit,
15956
+ req_id: kw.req_id
15957
+ }) < 0) (0, _yuneta_gobj_js.gobj_send_event)(src, "EV_PAGE_FAILED", {
15958
+ req_id: kw.req_id,
15959
+ error: "cannot reach the backend"
15960
+ }, gobj);
15961
+ return 0;
15962
+ }
15963
+ /********************************************
15918
15964
  * Event from formtable: ONE field of one record, edited in place.
15919
15965
  *
15920
15966
  * Written as a PARTIAL update and, deliberately, with NO `autolink`.
@@ -15981,6 +16027,11 @@ function ac_delete_record(gobj, event, kw, src) {
15981
16027
  * }
15982
16028
  ********************************************/
15983
16029
  function ac_refresh_topic(gobj, event, kw, src) {
16030
+ let gobj_topic_form = get_gobj_formtable(gobj, kw.topic_name);
16031
+ if (gobj_topic_form && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj_topic_form, "with_remote_paging")) {
16032
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_REPULL_PAGE", {}, gobj);
16033
+ return 0;
16034
+ }
15984
16035
  treedb_nodes$1(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name"), kw.topic_name, { list_dict: true });
15985
16036
  return 0;
15986
16037
  }
@@ -16070,6 +16121,11 @@ function create_gclass$6(gclass_name) {
16070
16121
  ac_update_field,
16071
16122
  null
16072
16123
  ],
16124
+ [
16125
+ "EV_REQUEST_PAGE",
16126
+ ac_request_page,
16127
+ null
16128
+ ],
16073
16129
  [
16074
16130
  "EV_DELETE_RECORD",
16075
16131
  ac_delete_record,
@@ -16144,6 +16200,7 @@ function create_gclass$6(gclass_name) {
16144
16200
  ["EV_CREATE_RECORD", 0],
16145
16201
  ["EV_UPDATE_RECORD", 0],
16146
16202
  ["EV_UPDATE_FIELD", 0],
16203
+ ["EV_REQUEST_PAGE", 0],
16147
16204
  ["EV_DELETE_RECORD", 0],
16148
16205
  ["EV_RECORD_WRITTEN", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
16149
16206
  ["EV_REFRESH_TOPIC", 0],
@@ -16358,6 +16415,8 @@ var attrs_table$5 = [
16358
16415
  (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)"),
16359
16416
  (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)"),
16360
16417
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_inline_edit", 0, true, "Edit a writable scalar cell in place while in edition mode (the record form keeps the rest)"),
16418
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Pull the topic a PAGE at a time from the backend (`nodes` from/limit) instead of loading it whole. Needs a backend that pages"),
16419
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_INTEGER, "page_size", 0, 200, "Rows per page when with_remote_paging. Generous on purpose: a treedb that fits in one page behaves exactly as it did, paginator hidden and every filter seeing every row"),
16361
16420
  (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"),
16362
16421
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16363
16422
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
@@ -16386,6 +16445,8 @@ var attrs_table$5 = [
16386
16445
  (0, _yuneta_gobj_js.SDATA_END)()
16387
16446
  ];
16388
16447
  var PRIVATE_DATA$5 = {
16448
+ _pending_pages: null,
16449
+ _page_seq: 0,
16389
16450
  $container: null,
16390
16451
  treedb_name: "",
16391
16452
  topic_name: "",
@@ -17111,6 +17172,25 @@ function create_tabulator(gobj) {
17111
17172
  let pkey = desc.pkey || "id";
17112
17173
  let selectable = with_checkbox ? "highlight" : with_radio ? 1 : false;
17113
17174
  let tabulator_settings = (0, _yuneta_gobj_js.json_deep_copy)((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator_settings"));
17175
+ if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_remote_paging")) {
17176
+ let page_size = (0, _yuneta_gobj_js.gobj_read_integer_attr)(gobj, "page_size") || 200;
17177
+ Object.assign(tabulator_settings, {
17178
+ pagination: true,
17179
+ paginationMode: "remote",
17180
+ filterMode: "local",
17181
+ paginationSize: page_size,
17182
+ paginationSizeSelector: [
17183
+ 50,
17184
+ 100,
17185
+ 200,
17186
+ 500
17187
+ ],
17188
+ ajaxURL: "nodes",
17189
+ ajaxRequestFunc: function(url, config, params) {
17190
+ return request_page(gobj, params.page || 1, params.size || page_size);
17191
+ }
17192
+ });
17193
+ }
17114
17194
  Object.assign(tabulator_settings, {
17115
17195
  index: pkey,
17116
17196
  columns,
@@ -18312,6 +18392,99 @@ function ac_show_schema(gobj, event, kw, src) {
18312
18392
  return 0;
18313
18393
  }
18314
18394
  /************************************************************
18395
+ * Ask the host for one page and park the promise Tabulator wants.
18396
+ *
18397
+ * The transport belongs to the HOST, so the request goes up as an event
18398
+ * and the answer comes back down as one; what lives here is only the
18399
+ * promise, keyed by a correlation id the host echoes.
18400
+ *
18401
+ * Rejected right away when there is nobody to ask: a request that can
18402
+ * never be answered would leave the table spinning for ever and leak
18403
+ * one entry per attempt. Same for the watchdog — the link can stay up
18404
+ * and the answer still never land.
18405
+ ************************************************************/
18406
+ var PAGE_TIMEOUT_MS = 2e4;
18407
+ function request_page(gobj, page, size) {
18408
+ let priv = gobj.priv;
18409
+ return new Promise(function(resolve, reject) {
18410
+ if (!priv._pending_pages) priv._pending_pages = {};
18411
+ let req_id = `p${++priv._page_seq}`;
18412
+ let timer = window.setTimeout(function() {
18413
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_PAGE_TIMEOUT", { req_id }, gobj);
18414
+ }, PAGE_TIMEOUT_MS);
18415
+ priv._pending_pages[req_id] = {
18416
+ resolve,
18417
+ reject,
18418
+ timer
18419
+ };
18420
+ (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_REQUEST_PAGE", {
18421
+ topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
18422
+ req_id,
18423
+ from: (page - 1) * size + 1,
18424
+ limit: size
18425
+ });
18426
+ });
18427
+ }
18428
+ /************************************************************
18429
+ * Settle a parked page request, however it ended.
18430
+ ************************************************************/
18431
+ function settle_page(gobj, req_id, value, error) {
18432
+ let priv = gobj.priv;
18433
+ let pend = priv._pending_pages ? priv._pending_pages[req_id] : null;
18434
+ if (!pend) return;
18435
+ delete priv._pending_pages[req_id];
18436
+ if (pend.timer) window.clearTimeout(pend.timer);
18437
+ if (error) {
18438
+ pend.reject(new Error(error));
18439
+ return;
18440
+ }
18441
+ pend.resolve(value);
18442
+ }
18443
+ /************************************************************
18444
+ * Pull the current page again. `replaceData()` re-runs
18445
+ * ajaxRequestFunc for the page the reader is on, so a refresh keeps
18446
+ * their position instead of throwing them back to the first page.
18447
+ ************************************************************/
18448
+ function ac_repull_page(gobj, event, kw, src) {
18449
+ let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
18450
+ if (!tabulator) return 0;
18451
+ try {
18452
+ tabulator.replaceData();
18453
+ } catch (e) {
18454
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot re-pull the page: ${e}`);
18455
+ return -1;
18456
+ }
18457
+ return 0;
18458
+ }
18459
+ /************************************************************
18460
+ * The host answered a page.
18461
+ ************************************************************/
18462
+ function ac_page_loaded(gobj, event, kw, src) {
18463
+ settle_page(gobj, kw.req_id, {
18464
+ data: (0, _yuneta_gobj_js.is_array)(kw.rows) ? kw.rows : [],
18465
+ last_page: kw.pages || 1,
18466
+ last_row: typeof kw.total === "number" ? kw.total : 0
18467
+ }, null);
18468
+ return 0;
18469
+ }
18470
+ /************************************************************
18471
+ * The host could not answer it.
18472
+ ************************************************************/
18473
+ function ac_page_failed(gobj, event, kw, src) {
18474
+ let why = kw && kw.error || "page request failed";
18475
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${why}`);
18476
+ settle_page(gobj, kw.req_id, null, why);
18477
+ return 0;
18478
+ }
18479
+ /************************************************************
18480
+ * Nobody answered in time.
18481
+ ************************************************************/
18482
+ function ac_page_timeout(gobj, event, kw, src) {
18483
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: no answer for page request '${kw.req_id}' in ${PAGE_TIMEOUT_MS}ms`);
18484
+ settle_page(gobj, kw.req_id, null, "page request timed out");
18485
+ return 0;
18486
+ }
18487
+ /************************************************************
18315
18488
  * Filter the loaded rows by a term matched against every non-internal
18316
18489
  * field. `clearFilter()` with no argument drops only THIS filter: the
18317
18490
  * per-column header filters are a separate layer and survive, so a
@@ -18559,6 +18732,26 @@ function create_gclass$5(gclass_name) {
18559
18732
  ac_cell_edited,
18560
18733
  null
18561
18734
  ],
18735
+ [
18736
+ "EV_REPULL_PAGE",
18737
+ ac_repull_page,
18738
+ null
18739
+ ],
18740
+ [
18741
+ "EV_PAGE_LOADED",
18742
+ ac_page_loaded,
18743
+ null
18744
+ ],
18745
+ [
18746
+ "EV_PAGE_FAILED",
18747
+ ac_page_failed,
18748
+ null
18749
+ ],
18750
+ [
18751
+ "EV_PAGE_TIMEOUT",
18752
+ ac_page_timeout,
18753
+ null
18754
+ ],
18562
18755
  [
18563
18756
  "EV_SEARCH",
18564
18757
  ac_search,
@@ -18610,7 +18803,12 @@ function create_gclass$5(gclass_name) {
18610
18803
  ["EV_REFRESH", 0],
18611
18804
  ["EV_SHOW_SCHEMA", 0],
18612
18805
  ["EV_CELL_EDITED", 0],
18806
+ ["EV_REPULL_PAGE", 0],
18807
+ ["EV_PAGE_LOADED", 0],
18808
+ ["EV_PAGE_FAILED", 0],
18809
+ ["EV_PAGE_TIMEOUT", 0],
18613
18810
  ["EV_UPDATE_FIELD", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
18811
+ ["EV_REQUEST_PAGE", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
18614
18812
  ["EV_SEARCH", 0],
18615
18813
  ["EV_OPEN_COLUMNS", 0],
18616
18814
  ["EV_TOGGLE_COLUMN", 0],
@@ -14492,6 +14492,7 @@ var attrs_table$6 = [
14492
14492
  SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote yuno for data fetching"),
14493
14493
  SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
14494
14494
  SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
14495
+ SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Each topic table pulls its rows a PAGE at a time (`nodes` from/limit) instead of loading the topic whole. Safe against a backend that cannot page: it answers the whole list, which the table reads as one page"),
14495
14496
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
14496
14497
  SDATA(data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
14497
14498
  SDATA(data_type_t.DTP_BOOLEAN, "with_cards_landing", 0, false, "Land on a grid of topic cards (list->detail): a card opens its table, with the tabs bar + a back-to-grid button. Off = tabs only (legacy)."),
@@ -15305,7 +15306,8 @@ function process_treedb_descs$1(gobj) {
15305
15306
  with_delete_button: !readonly,
15306
15307
  treedb_name,
15307
15308
  topic_name: key,
15308
- desc
15309
+ desc,
15310
+ with_remote_paging: gobj_read_bool_attr(gobj, "with_remote_paging")
15309
15311
  };
15310
15312
  let id = `${gobj_name(gobj)}?${desc.topic_name}`;
15311
15313
  let gobj_topic_form = gobj_create_service(id, "C_YUI_TREEDB_TOPIC_WITH_FORM", kw_topic_form, gobj);
@@ -15376,6 +15378,8 @@ function register_sub_routes$1(gobj) {
15376
15378
  function get_nodes$1(gobj, topic_name) {
15377
15379
  const treedb_name = gobj.priv.treedb_name;
15378
15380
  subscribe_treedb$1(gobj, topic_name);
15381
+ let gobj_topic_form = get_gobj_formtable(gobj, topic_name);
15382
+ if (gobj_topic_form && gobj_read_bool_attr(gobj_topic_form, "with_remote_paging")) return;
15379
15383
  treedb_nodes$1(gobj, treedb_name, topic_name, { list_dict: true });
15380
15384
  }
15381
15385
  /************************************************************
@@ -15423,7 +15427,7 @@ function get_gobj_formtable(gobj, topic_name) {
15423
15427
  /************************************************************
15424
15428
  * Command to remote service
15425
15429
  ************************************************************/
15426
- function treedb_nodes$1(gobj, treedb_name, topic_name, options) {
15430
+ function treedb_nodes$1(gobj, treedb_name, topic_name, options, page) {
15427
15431
  let command = "nodes";
15428
15432
  let gobj_remote_yuno = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
15429
15433
  let kw = {
@@ -15433,8 +15437,17 @@ function treedb_nodes$1(gobj, treedb_name, topic_name, options) {
15433
15437
  options: options || {}
15434
15438
  };
15435
15439
  kw.__md_command__ = { topic_name };
15440
+ if (page) {
15441
+ kw.from = page.from;
15442
+ kw.limit = page.limit;
15443
+ kw.__md_command__.req_id = page.req_id;
15444
+ }
15436
15445
  let ret = gobj_command(gobj_remote_yuno, command, kw, gobj);
15437
- if (ret) log_error(ret);
15446
+ if (ret) {
15447
+ log_error(ret);
15448
+ return -1;
15449
+ }
15450
+ return 0;
15438
15451
  }
15439
15452
  /************************************************************
15440
15453
  * Command to remote service
@@ -15668,7 +15681,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
15668
15681
  yui_shell_show_error(yui_shell_of(gobj), comment, { t });
15669
15682
  if (command === "update-node" || command === "create-node") {
15670
15683
  let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
15671
- if (failed_topic) treedb_nodes$1(gobj, gobj_read_str_attr(gobj, "treedb_name"), failed_topic, { list_dict: true });
15684
+ if (failed_topic) gobj_send_event(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
15672
15685
  }
15673
15686
  }
15674
15687
  return 0;
@@ -15684,7 +15697,15 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
15684
15697
  let topic_name = kw_get_str(gobj, kw_command, "topic_name", "", kw_flag_t.KW_REQUIRED);
15685
15698
  if (result >= 0) {
15686
15699
  let gobj_topic_form = gobj_find_child(gobj, { __gobj_name__: `${gobj_name(gobj)}?${topic_name}` });
15687
- gobj_send_event(gobj_topic_form, "EV_LOAD_NODES", nodes_answer(data).rows, gobj);
15700
+ let answer = nodes_answer(data);
15701
+ let req_id = kw_get_str(gobj, kw_command, "req_id", "", 0);
15702
+ if (req_id) gobj_send_event(gobj_topic_form, "EV_PAGE_LOADED", {
15703
+ req_id,
15704
+ rows: answer.rows,
15705
+ pages: answer.pages,
15706
+ total: answer.total
15707
+ }, gobj);
15708
+ else gobj_send_event(gobj_topic_form, "EV_LOAD_NODES", answer.rows, gobj);
15688
15709
  }
15689
15710
  break;
15690
15711
  case "create-node":
@@ -15910,6 +15931,31 @@ function ac_create_record(gobj, event, kw, src) {
15910
15931
  });
15911
15932
  }
15912
15933
  /********************************************
15934
+ * A topic table wants a page.
15935
+ *
15936
+ * The transport is ours, so the table asks and we fetch; the answer
15937
+ * finds its way back by the `req_id` we echo. A backend that cannot
15938
+ * page answers the whole list, which the table takes as one page —
15939
+ * which is the truth, and is why the table can ask without knowing
15940
+ * what it is talking to.
15941
+ ********************************************/
15942
+ function ac_request_page(gobj, event, kw, src) {
15943
+ let topic_name = kw.topic_name || gobj_read_attr(src, "topic_name");
15944
+ if (!topic_name || !kw.req_id) {
15945
+ log_error(`${gobj_short_name(gobj)}: a page request with no topic or no id`);
15946
+ return -1;
15947
+ }
15948
+ if (treedb_nodes$1(gobj, gobj_read_str_attr(gobj, "treedb_name"), topic_name, { list_dict: true }, {
15949
+ from: kw.from,
15950
+ limit: kw.limit,
15951
+ req_id: kw.req_id
15952
+ }) < 0) gobj_send_event(src, "EV_PAGE_FAILED", {
15953
+ req_id: kw.req_id,
15954
+ error: "cannot reach the backend"
15955
+ }, gobj);
15956
+ return 0;
15957
+ }
15958
+ /********************************************
15913
15959
  * Event from formtable: ONE field of one record, edited in place.
15914
15960
  *
15915
15961
  * Written as a PARTIAL update and, deliberately, with NO `autolink`.
@@ -15976,6 +16022,11 @@ function ac_delete_record(gobj, event, kw, src) {
15976
16022
  * }
15977
16023
  ********************************************/
15978
16024
  function ac_refresh_topic(gobj, event, kw, src) {
16025
+ let gobj_topic_form = get_gobj_formtable(gobj, kw.topic_name);
16026
+ if (gobj_topic_form && gobj_read_bool_attr(gobj_topic_form, "with_remote_paging")) {
16027
+ gobj_send_event(gobj_topic_form, "EV_REPULL_PAGE", {}, gobj);
16028
+ return 0;
16029
+ }
15979
16030
  treedb_nodes$1(gobj, gobj_read_str_attr(gobj, "treedb_name"), kw.topic_name, { list_dict: true });
15980
16031
  return 0;
15981
16032
  }
@@ -16065,6 +16116,11 @@ function create_gclass$6(gclass_name) {
16065
16116
  ac_update_field,
16066
16117
  null
16067
16118
  ],
16119
+ [
16120
+ "EV_REQUEST_PAGE",
16121
+ ac_request_page,
16122
+ null
16123
+ ],
16068
16124
  [
16069
16125
  "EV_DELETE_RECORD",
16070
16126
  ac_delete_record,
@@ -16139,6 +16195,7 @@ function create_gclass$6(gclass_name) {
16139
16195
  ["EV_CREATE_RECORD", 0],
16140
16196
  ["EV_UPDATE_RECORD", 0],
16141
16197
  ["EV_UPDATE_FIELD", 0],
16198
+ ["EV_REQUEST_PAGE", 0],
16142
16199
  ["EV_DELETE_RECORD", 0],
16143
16200
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
16144
16201
  ["EV_REFRESH_TOPIC", 0],
@@ -16353,6 +16410,8 @@ var attrs_table$5 = [
16353
16410
  SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
16354
16411
  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)"),
16355
16412
  SDATA(data_type_t.DTP_BOOLEAN, "with_inline_edit", 0, true, "Edit a writable scalar cell in place while in edition mode (the record form keeps the rest)"),
16413
+ SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Pull the topic a PAGE at a time from the backend (`nodes` from/limit) instead of loading it whole. Needs a backend that pages"),
16414
+ SDATA(data_type_t.DTP_INTEGER, "page_size", 0, 200, "Rows per page when with_remote_paging. Generous on purpose: a treedb that fits in one page behaves exactly as it did, paginator hidden and every filter seeing every row"),
16356
16415
  SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
16357
16416
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16358
16417
  SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
@@ -16381,6 +16440,8 @@ var attrs_table$5 = [
16381
16440
  SDATA_END()
16382
16441
  ];
16383
16442
  var PRIVATE_DATA$5 = {
16443
+ _pending_pages: null,
16444
+ _page_seq: 0,
16384
16445
  $container: null,
16385
16446
  treedb_name: "",
16386
16447
  topic_name: "",
@@ -17106,6 +17167,25 @@ function create_tabulator(gobj) {
17106
17167
  let pkey = desc.pkey || "id";
17107
17168
  let selectable = with_checkbox ? "highlight" : with_radio ? 1 : false;
17108
17169
  let tabulator_settings = json_deep_copy(gobj_read_attr(gobj, "tabulator_settings"));
17170
+ if (gobj_read_bool_attr(gobj, "with_remote_paging")) {
17171
+ let page_size = gobj_read_integer_attr(gobj, "page_size") || 200;
17172
+ Object.assign(tabulator_settings, {
17173
+ pagination: true,
17174
+ paginationMode: "remote",
17175
+ filterMode: "local",
17176
+ paginationSize: page_size,
17177
+ paginationSizeSelector: [
17178
+ 50,
17179
+ 100,
17180
+ 200,
17181
+ 500
17182
+ ],
17183
+ ajaxURL: "nodes",
17184
+ ajaxRequestFunc: function(url, config, params) {
17185
+ return request_page(gobj, params.page || 1, params.size || page_size);
17186
+ }
17187
+ });
17188
+ }
17109
17189
  Object.assign(tabulator_settings, {
17110
17190
  index: pkey,
17111
17191
  columns,
@@ -18307,6 +18387,99 @@ function ac_show_schema(gobj, event, kw, src) {
18307
18387
  return 0;
18308
18388
  }
18309
18389
  /************************************************************
18390
+ * Ask the host for one page and park the promise Tabulator wants.
18391
+ *
18392
+ * The transport belongs to the HOST, so the request goes up as an event
18393
+ * and the answer comes back down as one; what lives here is only the
18394
+ * promise, keyed by a correlation id the host echoes.
18395
+ *
18396
+ * Rejected right away when there is nobody to ask: a request that can
18397
+ * never be answered would leave the table spinning for ever and leak
18398
+ * one entry per attempt. Same for the watchdog — the link can stay up
18399
+ * and the answer still never land.
18400
+ ************************************************************/
18401
+ var PAGE_TIMEOUT_MS = 2e4;
18402
+ function request_page(gobj, page, size) {
18403
+ let priv = gobj.priv;
18404
+ return new Promise(function(resolve, reject) {
18405
+ if (!priv._pending_pages) priv._pending_pages = {};
18406
+ let req_id = `p${++priv._page_seq}`;
18407
+ let timer = window.setTimeout(function() {
18408
+ gobj_send_event(gobj, "EV_PAGE_TIMEOUT", { req_id }, gobj);
18409
+ }, PAGE_TIMEOUT_MS);
18410
+ priv._pending_pages[req_id] = {
18411
+ resolve,
18412
+ reject,
18413
+ timer
18414
+ };
18415
+ gobj_publish_event(gobj, "EV_REQUEST_PAGE", {
18416
+ topic_name: gobj_read_str_attr(gobj, "topic_name"),
18417
+ req_id,
18418
+ from: (page - 1) * size + 1,
18419
+ limit: size
18420
+ });
18421
+ });
18422
+ }
18423
+ /************************************************************
18424
+ * Settle a parked page request, however it ended.
18425
+ ************************************************************/
18426
+ function settle_page(gobj, req_id, value, error) {
18427
+ let priv = gobj.priv;
18428
+ let pend = priv._pending_pages ? priv._pending_pages[req_id] : null;
18429
+ if (!pend) return;
18430
+ delete priv._pending_pages[req_id];
18431
+ if (pend.timer) window.clearTimeout(pend.timer);
18432
+ if (error) {
18433
+ pend.reject(new Error(error));
18434
+ return;
18435
+ }
18436
+ pend.resolve(value);
18437
+ }
18438
+ /************************************************************
18439
+ * Pull the current page again. `replaceData()` re-runs
18440
+ * ajaxRequestFunc for the page the reader is on, so a refresh keeps
18441
+ * their position instead of throwing them back to the first page.
18442
+ ************************************************************/
18443
+ function ac_repull_page(gobj, event, kw, src) {
18444
+ let tabulator = gobj_read_attr(gobj, "tabulator");
18445
+ if (!tabulator) return 0;
18446
+ try {
18447
+ tabulator.replaceData();
18448
+ } catch (e) {
18449
+ log_error(`${gobj_short_name(gobj)}: cannot re-pull the page: ${e}`);
18450
+ return -1;
18451
+ }
18452
+ return 0;
18453
+ }
18454
+ /************************************************************
18455
+ * The host answered a page.
18456
+ ************************************************************/
18457
+ function ac_page_loaded(gobj, event, kw, src) {
18458
+ settle_page(gobj, kw.req_id, {
18459
+ data: is_array(kw.rows) ? kw.rows : [],
18460
+ last_page: kw.pages || 1,
18461
+ last_row: typeof kw.total === "number" ? kw.total : 0
18462
+ }, null);
18463
+ return 0;
18464
+ }
18465
+ /************************************************************
18466
+ * The host could not answer it.
18467
+ ************************************************************/
18468
+ function ac_page_failed(gobj, event, kw, src) {
18469
+ let why = kw && kw.error || "page request failed";
18470
+ log_error(`${gobj_short_name(gobj)}: ${why}`);
18471
+ settle_page(gobj, kw.req_id, null, why);
18472
+ return 0;
18473
+ }
18474
+ /************************************************************
18475
+ * Nobody answered in time.
18476
+ ************************************************************/
18477
+ function ac_page_timeout(gobj, event, kw, src) {
18478
+ log_error(`${gobj_short_name(gobj)}: no answer for page request '${kw.req_id}' in ${PAGE_TIMEOUT_MS}ms`);
18479
+ settle_page(gobj, kw.req_id, null, "page request timed out");
18480
+ return 0;
18481
+ }
18482
+ /************************************************************
18310
18483
  * Filter the loaded rows by a term matched against every non-internal
18311
18484
  * field. `clearFilter()` with no argument drops only THIS filter: the
18312
18485
  * per-column header filters are a separate layer and survive, so a
@@ -18554,6 +18727,26 @@ function create_gclass$5(gclass_name) {
18554
18727
  ac_cell_edited,
18555
18728
  null
18556
18729
  ],
18730
+ [
18731
+ "EV_REPULL_PAGE",
18732
+ ac_repull_page,
18733
+ null
18734
+ ],
18735
+ [
18736
+ "EV_PAGE_LOADED",
18737
+ ac_page_loaded,
18738
+ null
18739
+ ],
18740
+ [
18741
+ "EV_PAGE_FAILED",
18742
+ ac_page_failed,
18743
+ null
18744
+ ],
18745
+ [
18746
+ "EV_PAGE_TIMEOUT",
18747
+ ac_page_timeout,
18748
+ null
18749
+ ],
18557
18750
  [
18558
18751
  "EV_SEARCH",
18559
18752
  ac_search,
@@ -18605,7 +18798,12 @@ function create_gclass$5(gclass_name) {
18605
18798
  ["EV_REFRESH", 0],
18606
18799
  ["EV_SHOW_SCHEMA", 0],
18607
18800
  ["EV_CELL_EDITED", 0],
18801
+ ["EV_REPULL_PAGE", 0],
18802
+ ["EV_PAGE_LOADED", 0],
18803
+ ["EV_PAGE_FAILED", 0],
18804
+ ["EV_PAGE_TIMEOUT", 0],
18608
18805
  ["EV_UPDATE_FIELD", event_flag_t.EVF_OUTPUT_EVENT],
18806
+ ["EV_REQUEST_PAGE", event_flag_t.EVF_OUTPUT_EVENT],
18609
18807
  ["EV_SEARCH", 0],
18610
18808
  ["EV_OPEN_COLUMNS", 0],
18611
18809
  ["EV_TOGGLE_COLUMN", 0],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.8.1",
3
+ "version": "7.9.1",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -48,6 +48,7 @@ import {
48
48
  gclass_find_by_name,
49
49
  gobj_write_attr,
50
50
  gobj_read_bool_attr,
51
+ gobj_read_integer_attr,
51
52
  gobj_read_str_attr,
52
53
  gobj_write_bool_attr,
53
54
  gobj_publish_event,
@@ -112,6 +113,8 @@ SDATA(data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button
112
113
  SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
113
114
  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)"),
114
115
  SDATA(data_type_t.DTP_BOOLEAN, "with_inline_edit", 0, true, "Edit a writable scalar cell in place while in edition mode (the record form keeps the rest)"),
116
+ SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Pull the topic a PAGE at a time from the backend (`nodes` from/limit) instead of loading it whole. Needs a backend that pages"),
117
+ SDATA(data_type_t.DTP_INTEGER, "page_size", 0, 200, "Rows per page when with_remote_paging. Generous on purpose: a treedb that fits in one page behaves exactly as it did, paginator hidden and every filter seeing every row"),
115
118
  SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
116
119
 
117
120
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
@@ -146,6 +149,8 @@ SDATA_END()
146
149
  ];
147
150
 
148
151
  let PRIVATE_DATA = {
152
+ _pending_pages: null, // req_id -> {resolve, reject, timer}
153
+ _page_seq: 0, // correlation id of a page request
149
154
  $container: null,
150
155
  treedb_name: "",
151
156
  topic_name: "",
@@ -1095,6 +1100,36 @@ function create_tabulator(gobj)
1095
1100
 
1096
1101
  let tabulator_settings = json_deep_copy(gobj_read_attr(gobj, "tabulator_settings"));
1097
1102
 
1103
+ /* Remote paging: the TABLE pulls, instead of the host pushing the whole
1104
+ * topic down. The page size is generous on purpose — a treedb that fits
1105
+ * in one page behaves exactly as it did before, with the paginator
1106
+ * hidden and every filter seeing every row. Only a topic that does NOT
1107
+ * fit pays for paging, and for that one loading it whole was never an
1108
+ * option anyway.
1109
+ *
1110
+ * `filterMode: "local"` says the plain truth: the header filters and the
1111
+ * search box work on the page that is loaded. Same as the tranger
1112
+ * browser's Rows card, and for the same reason — the alternative is
1113
+ * pushing every filter to the backend and changing what "search" means.
1114
+ */
1115
+ if(gobj_read_bool_attr(gobj, "with_remote_paging")) {
1116
+ let page_size = gobj_read_integer_attr(gobj, "page_size") || 200;
1117
+ Object.assign(tabulator_settings, {
1118
+ pagination: true,
1119
+ paginationMode: "remote",
1120
+ filterMode: "local",
1121
+ paginationSize: page_size,
1122
+ paginationSizeSelector: [50, 100, 200, 500],
1123
+ ajaxURL: "nodes", /* dummy: only fires the func */
1124
+ ajaxRequestFunc: function(url, config, params) {
1125
+ /* Widget plumbing, not an action: Tabulator wants a PROMISE
1126
+ * back — it is a data source. The action is the event this
1127
+ * parks on. */
1128
+ return request_page(gobj, params.page || 1, params.size || page_size);
1129
+ }
1130
+ });
1131
+ }
1132
+
1098
1133
  Object.assign(tabulator_settings, {
1099
1134
  index: pkey,
1100
1135
  columns: columns,
@@ -2965,6 +3000,125 @@ function ac_show_schema(gobj, event, kw, src)
2965
3000
  return 0;
2966
3001
  }
2967
3002
 
3003
+ /************************************************************
3004
+ * Ask the host for one page and park the promise Tabulator wants.
3005
+ *
3006
+ * The transport belongs to the HOST, so the request goes up as an event
3007
+ * and the answer comes back down as one; what lives here is only the
3008
+ * promise, keyed by a correlation id the host echoes.
3009
+ *
3010
+ * Rejected right away when there is nobody to ask: a request that can
3011
+ * never be answered would leave the table spinning for ever and leak
3012
+ * one entry per attempt. Same for the watchdog — the link can stay up
3013
+ * and the answer still never land.
3014
+ ************************************************************/
3015
+ const PAGE_TIMEOUT_MS = 20000;
3016
+
3017
+ function request_page(gobj, page, size)
3018
+ {
3019
+ let priv = gobj.priv;
3020
+
3021
+ return new Promise(function(resolve, reject) {
3022
+ if(!priv._pending_pages) {
3023
+ priv._pending_pages = {};
3024
+ }
3025
+ let req_id = `p${++priv._page_seq}`;
3026
+
3027
+ let timer = window.setTimeout(function() {
3028
+ gobj_send_event(gobj, "EV_PAGE_TIMEOUT", {req_id: req_id}, gobj);
3029
+ }, PAGE_TIMEOUT_MS);
3030
+
3031
+ priv._pending_pages[req_id] = {resolve: resolve, reject: reject, timer: timer};
3032
+
3033
+ gobj_publish_event(
3034
+ gobj,
3035
+ "EV_REQUEST_PAGE",
3036
+ {
3037
+ topic_name: gobj_read_str_attr(gobj, "topic_name"),
3038
+ req_id: req_id,
3039
+ from: (page - 1) * size + 1, /* `nodes` counts from 1 */
3040
+ limit: size
3041
+ }
3042
+ );
3043
+ });
3044
+ }
3045
+
3046
+ /************************************************************
3047
+ * Settle a parked page request, however it ended.
3048
+ ************************************************************/
3049
+ function settle_page(gobj, req_id, value, error)
3050
+ {
3051
+ let priv = gobj.priv;
3052
+ let pend = priv._pending_pages? priv._pending_pages[req_id] : null;
3053
+ if(!pend) {
3054
+ return; /* already settled: a late answer after a timeout */
3055
+ }
3056
+ delete priv._pending_pages[req_id];
3057
+ if(pend.timer) {
3058
+ window.clearTimeout(pend.timer);
3059
+ }
3060
+ if(error) {
3061
+ pend.reject(new Error(error));
3062
+ return;
3063
+ }
3064
+ pend.resolve(value);
3065
+ }
3066
+
3067
+ /************************************************************
3068
+ * Pull the current page again. `replaceData()` re-runs
3069
+ * ajaxRequestFunc for the page the reader is on, so a refresh keeps
3070
+ * their position instead of throwing them back to the first page.
3071
+ ************************************************************/
3072
+ function ac_repull_page(gobj, event, kw, src)
3073
+ {
3074
+ let tabulator = gobj_read_attr(gobj, "tabulator");
3075
+ if(!tabulator) {
3076
+ return 0;
3077
+ }
3078
+ try {
3079
+ tabulator.replaceData();
3080
+ } catch(e) {
3081
+ log_error(`${gobj_short_name(gobj)}: cannot re-pull the page: ${e}`);
3082
+ return -1;
3083
+ }
3084
+ return 0;
3085
+ }
3086
+
3087
+ /************************************************************
3088
+ * The host answered a page.
3089
+ ************************************************************/
3090
+ function ac_page_loaded(gobj, event, kw, src)
3091
+ {
3092
+ settle_page(gobj, kw.req_id, {
3093
+ data: is_array(kw.rows)? kw.rows : [],
3094
+ last_page: kw.pages || 1,
3095
+ last_row: (typeof kw.total === "number")? kw.total : 0
3096
+ }, null);
3097
+ return 0;
3098
+ }
3099
+
3100
+ /************************************************************
3101
+ * The host could not answer it.
3102
+ ************************************************************/
3103
+ function ac_page_failed(gobj, event, kw, src)
3104
+ {
3105
+ let why = (kw && kw.error) || "page request failed";
3106
+ log_error(`${gobj_short_name(gobj)}: ${why}`);
3107
+ settle_page(gobj, kw.req_id, null, why);
3108
+ return 0;
3109
+ }
3110
+
3111
+ /************************************************************
3112
+ * Nobody answered in time.
3113
+ ************************************************************/
3114
+ function ac_page_timeout(gobj, event, kw, src)
3115
+ {
3116
+ log_error(`${gobj_short_name(gobj)}: no answer for page request ` +
3117
+ `'${kw.req_id}' in ${PAGE_TIMEOUT_MS}ms`);
3118
+ settle_page(gobj, kw.req_id, null, "page request timed out");
3119
+ return 0;
3120
+ }
3121
+
2968
3122
  /************************************************************
2969
3123
  * Filter the loaded rows by a term matched against every non-internal
2970
3124
  * field. `clearFilter()` with no argument drops only THIS filter: the
@@ -3187,6 +3341,10 @@ function create_gclass(gclass_name)
3187
3341
  ["EV_REFRESH", ac_refresh, null],
3188
3342
  ["EV_SHOW_SCHEMA", ac_show_schema, null],
3189
3343
  ["EV_CELL_EDITED", ac_cell_edited, null],
3344
+ ["EV_REPULL_PAGE", ac_repull_page, null],
3345
+ ["EV_PAGE_LOADED", ac_page_loaded, null],
3346
+ ["EV_PAGE_FAILED", ac_page_failed, null],
3347
+ ["EV_PAGE_TIMEOUT", ac_page_timeout, null],
3190
3348
  ["EV_SEARCH", ac_search, null],
3191
3349
  ["EV_OPEN_COLUMNS", ac_open_columns, null],
3192
3350
  ["EV_TOGGLE_COLUMN", ac_toggle_column, null],
@@ -3220,7 +3378,12 @@ function create_gclass(gclass_name)
3220
3378
  ["EV_REFRESH", 0],
3221
3379
  ["EV_SHOW_SCHEMA", 0],
3222
3380
  ["EV_CELL_EDITED", 0],
3381
+ ["EV_REPULL_PAGE", 0],
3382
+ ["EV_PAGE_LOADED", 0],
3383
+ ["EV_PAGE_FAILED", 0],
3384
+ ["EV_PAGE_TIMEOUT", 0],
3223
3385
  ["EV_UPDATE_FIELD", event_flag_t.EVF_OUTPUT_EVENT],
3386
+ ["EV_REQUEST_PAGE", event_flag_t.EVF_OUTPUT_EVENT],
3224
3387
  ["EV_SEARCH", 0],
3225
3388
  ["EV_OPEN_COLUMNS", 0],
3226
3389
  ["EV_TOGGLE_COLUMN", 0],
@@ -75,6 +75,7 @@ SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of o
75
75
  SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote yuno for data fetching"),
76
76
  SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
77
77
  SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Description of topics"),
78
+ SDATA(data_type_t.DTP_BOOLEAN, "with_remote_paging", 0, false, "Each topic table pulls its rows a PAGE at a time (`nodes` from/limit) instead of loading the topic whole. Safe against a backend that cannot page: it answers the whole list, which the table reads as one page"),
78
79
  SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
79
80
  SDATA(data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
80
81
  SDATA(data_type_t.DTP_BOOLEAN, "with_cards_landing",0, false, "Land on a grid of topic cards (list->detail): a card opens its table, with the tabs bar + a back-to-grid button. Off = tabs only (legacy)."),
@@ -1037,7 +1038,8 @@ function process_treedb_descs(gobj)
1037
1038
 
1038
1039
  treedb_name: treedb_name,
1039
1040
  topic_name: key,
1040
- desc: desc
1041
+ desc: desc,
1042
+ with_remote_paging: gobj_read_bool_attr(gobj, "with_remote_paging")
1041
1043
  };
1042
1044
 
1043
1045
  let id = `${gobj_name(gobj)}?${desc.topic_name}`;
@@ -1171,8 +1173,15 @@ function get_nodes(gobj, topic_name)
1171
1173
  subscribe_treedb(gobj, topic_name);
1172
1174
 
1173
1175
  /*
1174
- * Get data
1176
+ * Get data — unless the table pulls its own pages, in which case it has
1177
+ * already asked (or is about to) and a second whole-topic fetch would be
1178
+ * the very transfer paging exists to avoid.
1175
1179
  */
1180
+ let gobj_topic_form = get_gobj_formtable(gobj, topic_name);
1181
+ if(gobj_topic_form && gobj_read_bool_attr(gobj_topic_form, "with_remote_paging")) {
1182
+ return;
1183
+ }
1184
+
1176
1185
  treedb_nodes(
1177
1186
  gobj,
1178
1187
  treedb_name,
@@ -1328,7 +1337,7 @@ function get_gobj_formtable(gobj, topic_name)
1328
1337
  /************************************************************
1329
1338
  * Command to remote service
1330
1339
  ************************************************************/
1331
- function treedb_nodes(gobj, treedb_name, topic_name, options)
1340
+ function treedb_nodes(gobj, treedb_name, topic_name, options, page)
1332
1341
  {
1333
1342
  let command = "nodes";
1334
1343
 
@@ -1344,6 +1353,16 @@ function treedb_nodes(gobj, treedb_name, topic_name, options)
1344
1353
  topic_name: topic_name,
1345
1354
  };
1346
1355
 
1356
+ /* A PAGE, when the table asked for one. `from` is 1-based and a
1357
+ * backend that does not know these two answers the whole list, which
1358
+ * nodes_answer() reads as one page — so an old backend degrades to
1359
+ * exactly what it always did. */
1360
+ if(page) {
1361
+ kw.from = page.from;
1362
+ kw.limit = page.limit;
1363
+ kw.__md_command__.req_id = page.req_id;
1364
+ }
1365
+
1347
1366
  // TODO review msg_iev_write_key(kw, "__topic_name__", topic_name);
1348
1367
 
1349
1368
  let ret = gobj_command(
@@ -1354,7 +1373,9 @@ function treedb_nodes(gobj, treedb_name, topic_name, options)
1354
1373
  );
1355
1374
  if(ret) {
1356
1375
  log_error(ret);
1376
+ return -1;
1357
1377
  }
1378
+ return 0;
1358
1379
  }
1359
1380
 
1360
1381
  /************************************************************
@@ -1749,8 +1770,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
1749
1770
  if(command === "update-node" || command === "create-node") {
1750
1771
  let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
1751
1772
  if(failed_topic) {
1752
- treedb_nodes(gobj, gobj_read_str_attr(gobj, "treedb_name"),
1753
- failed_topic, {list_dict: true});
1773
+ gobj_send_event(gobj, "EV_REFRESH_TOPIC",
1774
+ {topic_name: failed_topic}, gobj);
1754
1775
  }
1755
1776
  }
1756
1777
  }
@@ -1778,13 +1799,38 @@ function ac_mt_command_answer(gobj, event, kw, src)
1778
1799
  * answers the plain list whatever is asked of it. Reading
1779
1800
  * the shape rather than assuming it is what keeps the table
1780
1801
  * from rendering an object's keys as rows the day a backend
1781
- * upgrades. */
1782
- gobj_send_event(
1783
- gobj_topic_form,
1784
- "EV_LOAD_NODES",
1785
- nodes_answer(data).rows,
1786
- gobj
1787
- );
1802
+ * upgrades — and it is what lets a paging table survive a
1803
+ * backend that cannot page: the whole list arrives as one
1804
+ * page, which is the truth. */
1805
+ let answer = nodes_answer(data);
1806
+ /* The correlation id rides in `__md_command__`, which is the
1807
+ * same place `topic_name` rides — and `kw_command` IS that
1808
+ * object: C_IEVENT_CLI EXTRACTS `__md_command__` from the kw
1809
+ * and pushes it as the command stack's `kw`. So it is read
1810
+ * flat, one level up from where the sent kw put it, and it
1811
+ * never travels to the backend as a parameter it does not
1812
+ * know. */
1813
+ let req_id = kw_get_str(gobj, kw_command, "req_id", "", 0);
1814
+ if(req_id) {
1815
+ gobj_send_event(
1816
+ gobj_topic_form,
1817
+ "EV_PAGE_LOADED",
1818
+ {
1819
+ req_id: req_id,
1820
+ rows: answer.rows,
1821
+ pages: answer.pages,
1822
+ total: answer.total
1823
+ },
1824
+ gobj
1825
+ );
1826
+ } else {
1827
+ gobj_send_event(
1828
+ gobj_topic_form,
1829
+ "EV_LOAD_NODES",
1830
+ answer.rows,
1831
+ gobj
1832
+ );
1833
+ }
1788
1834
  }
1789
1835
  break;
1790
1836
 
@@ -2147,6 +2193,40 @@ function ac_create_record(gobj, event, kw, src)
2147
2193
  );
2148
2194
  }
2149
2195
 
2196
+ /********************************************
2197
+ * A topic table wants a page.
2198
+ *
2199
+ * The transport is ours, so the table asks and we fetch; the answer
2200
+ * finds its way back by the `req_id` we echo. A backend that cannot
2201
+ * page answers the whole list, which the table takes as one page —
2202
+ * which is the truth, and is why the table can ask without knowing
2203
+ * what it is talking to.
2204
+ ********************************************/
2205
+ function ac_request_page(gobj, event, kw, src)
2206
+ {
2207
+ let topic_name = kw.topic_name || gobj_read_attr(src, "topic_name");
2208
+ if(!topic_name || !kw.req_id) {
2209
+ log_error(`${gobj_short_name(gobj)}: a page request with no topic or no id`);
2210
+ return -1;
2211
+ }
2212
+
2213
+ let ret = treedb_nodes(
2214
+ gobj,
2215
+ gobj_read_str_attr(gobj, "treedb_name"),
2216
+ topic_name,
2217
+ {list_dict: true},
2218
+ {from: kw.from, limit: kw.limit, req_id: kw.req_id}
2219
+ );
2220
+
2221
+ if(ret < 0) {
2222
+ /* The command never went out, so no answer will ever come: tell the
2223
+ * table now instead of leaving it spinning until its watchdog. */
2224
+ gobj_send_event(src, "EV_PAGE_FAILED",
2225
+ {req_id: kw.req_id, error: "cannot reach the backend"}, gobj);
2226
+ }
2227
+ return 0;
2228
+ }
2229
+
2150
2230
  /********************************************
2151
2231
  * Event from formtable: ONE field of one record, edited in place.
2152
2232
  *
@@ -2256,6 +2336,14 @@ function ac_delete_record(gobj, event, kw, src)
2256
2336
  ********************************************/
2257
2337
  function ac_refresh_topic(gobj, event, kw, src)
2258
2338
  {
2339
+ let gobj_topic_form = get_gobj_formtable(gobj, kw.topic_name);
2340
+ if(gobj_topic_form && gobj_read_bool_attr(gobj_topic_form, "with_remote_paging")) {
2341
+ /* A table that pulls refreshes by pulling: pushing a whole topic
2342
+ * into a paged table would replace its page with everything. */
2343
+ gobj_send_event(gobj_topic_form, "EV_REPULL_PAGE", {}, gobj);
2344
+ return 0;
2345
+ }
2346
+
2259
2347
  let options = {
2260
2348
  list_dict: true
2261
2349
  };
@@ -2357,6 +2445,7 @@ function create_gclass(gclass_name)
2357
2445
  ["EV_CREATE_RECORD", ac_create_record, null],
2358
2446
  ["EV_UPDATE_RECORD", ac_update_record, null],
2359
2447
  ["EV_UPDATE_FIELD", ac_update_field, null],
2448
+ ["EV_REQUEST_PAGE", ac_request_page, null],
2360
2449
  ["EV_DELETE_RECORD", ac_delete_record, null],
2361
2450
  ["EV_REFRESH_TOPIC", ac_refresh_topic, null],
2362
2451
  ["EV_OPEN_JSON", ac_open_json, null],
@@ -2384,6 +2473,7 @@ function create_gclass(gclass_name)
2384
2473
  ["EV_CREATE_RECORD", 0],
2385
2474
  ["EV_UPDATE_RECORD", 0],
2386
2475
  ["EV_UPDATE_FIELD", 0],
2476
+ ["EV_REQUEST_PAGE", 0],
2387
2477
  ["EV_DELETE_RECORD", 0],
2388
2478
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT|
2389
2479
  event_flag_t.EVF_NO_WARN_SUBS],