@yuneta/gobj-ui 7.8.1 → 7.9.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.
@@ -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,16 @@ 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 md = (0, _yuneta_gobj_js.kw_get_dict)(gobj, kw_command, "__md_command__", {}, 0);
15707
+ let req_id = (0, _yuneta_gobj_js.kw_get_str)(gobj, md, "req_id", "", 0);
15708
+ if (req_id) (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_PAGE_LOADED", {
15709
+ req_id,
15710
+ rows: answer.rows,
15711
+ pages: answer.pages,
15712
+ total: answer.total
15713
+ }, gobj);
15714
+ else (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_LOAD_NODES", answer.rows, gobj);
15693
15715
  }
15694
15716
  break;
15695
15717
  case "create-node":
@@ -15915,6 +15937,31 @@ function ac_create_record(gobj, event, kw, src) {
15915
15937
  });
15916
15938
  }
15917
15939
  /********************************************
15940
+ * A topic table wants a page.
15941
+ *
15942
+ * The transport is ours, so the table asks and we fetch; the answer
15943
+ * finds its way back by the `req_id` we echo. A backend that cannot
15944
+ * page answers the whole list, which the table takes as one page —
15945
+ * which is the truth, and is why the table can ask without knowing
15946
+ * what it is talking to.
15947
+ ********************************************/
15948
+ function ac_request_page(gobj, event, kw, src) {
15949
+ let topic_name = kw.topic_name || (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
15950
+ if (!topic_name || !kw.req_id) {
15951
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: a page request with no topic or no id`);
15952
+ return -1;
15953
+ }
15954
+ if (treedb_nodes$1(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name"), topic_name, { list_dict: true }, {
15955
+ from: kw.from,
15956
+ limit: kw.limit,
15957
+ req_id: kw.req_id
15958
+ }) < 0) (0, _yuneta_gobj_js.gobj_send_event)(src, "EV_PAGE_FAILED", {
15959
+ req_id: kw.req_id,
15960
+ error: "cannot reach the backend"
15961
+ }, gobj);
15962
+ return 0;
15963
+ }
15964
+ /********************************************
15918
15965
  * Event from formtable: ONE field of one record, edited in place.
15919
15966
  *
15920
15967
  * Written as a PARTIAL update and, deliberately, with NO `autolink`.
@@ -15981,6 +16028,11 @@ function ac_delete_record(gobj, event, kw, src) {
15981
16028
  * }
15982
16029
  ********************************************/
15983
16030
  function ac_refresh_topic(gobj, event, kw, src) {
16031
+ let gobj_topic_form = get_gobj_formtable(gobj, kw.topic_name);
16032
+ if (gobj_topic_form && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj_topic_form, "with_remote_paging")) {
16033
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_REPULL_PAGE", {}, gobj);
16034
+ return 0;
16035
+ }
15984
16036
  treedb_nodes$1(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name"), kw.topic_name, { list_dict: true });
15985
16037
  return 0;
15986
16038
  }
@@ -16070,6 +16122,11 @@ function create_gclass$6(gclass_name) {
16070
16122
  ac_update_field,
16071
16123
  null
16072
16124
  ],
16125
+ [
16126
+ "EV_REQUEST_PAGE",
16127
+ ac_request_page,
16128
+ null
16129
+ ],
16073
16130
  [
16074
16131
  "EV_DELETE_RECORD",
16075
16132
  ac_delete_record,
@@ -16144,6 +16201,7 @@ function create_gclass$6(gclass_name) {
16144
16201
  ["EV_CREATE_RECORD", 0],
16145
16202
  ["EV_UPDATE_RECORD", 0],
16146
16203
  ["EV_UPDATE_FIELD", 0],
16204
+ ["EV_REQUEST_PAGE", 0],
16147
16205
  ["EV_DELETE_RECORD", 0],
16148
16206
  ["EV_RECORD_WRITTEN", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
16149
16207
  ["EV_REFRESH_TOPIC", 0],
@@ -16358,6 +16416,8 @@ var attrs_table$5 = [
16358
16416
  (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
16417
  (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
16418
  (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)"),
16419
+ (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"),
16420
+ (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
16421
  (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
16422
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16363
16423
  (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 +16446,8 @@ var attrs_table$5 = [
16386
16446
  (0, _yuneta_gobj_js.SDATA_END)()
16387
16447
  ];
16388
16448
  var PRIVATE_DATA$5 = {
16449
+ _pending_pages: null,
16450
+ _page_seq: 0,
16389
16451
  $container: null,
16390
16452
  treedb_name: "",
16391
16453
  topic_name: "",
@@ -17111,6 +17173,25 @@ function create_tabulator(gobj) {
17111
17173
  let pkey = desc.pkey || "id";
17112
17174
  let selectable = with_checkbox ? "highlight" : with_radio ? 1 : false;
17113
17175
  let tabulator_settings = (0, _yuneta_gobj_js.json_deep_copy)((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator_settings"));
17176
+ if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_remote_paging")) {
17177
+ let page_size = (0, _yuneta_gobj_js.gobj_read_integer_attr)(gobj, "page_size") || 200;
17178
+ Object.assign(tabulator_settings, {
17179
+ pagination: true,
17180
+ paginationMode: "remote",
17181
+ filterMode: "local",
17182
+ paginationSize: page_size,
17183
+ paginationSizeSelector: [
17184
+ 50,
17185
+ 100,
17186
+ 200,
17187
+ 500
17188
+ ],
17189
+ ajaxURL: "nodes",
17190
+ ajaxRequestFunc: function(url, config, params) {
17191
+ return request_page(gobj, params.page || 1, params.size || page_size);
17192
+ }
17193
+ });
17194
+ }
17114
17195
  Object.assign(tabulator_settings, {
17115
17196
  index: pkey,
17116
17197
  columns,
@@ -18312,6 +18393,99 @@ function ac_show_schema(gobj, event, kw, src) {
18312
18393
  return 0;
18313
18394
  }
18314
18395
  /************************************************************
18396
+ * Ask the host for one page and park the promise Tabulator wants.
18397
+ *
18398
+ * The transport belongs to the HOST, so the request goes up as an event
18399
+ * and the answer comes back down as one; what lives here is only the
18400
+ * promise, keyed by a correlation id the host echoes.
18401
+ *
18402
+ * Rejected right away when there is nobody to ask: a request that can
18403
+ * never be answered would leave the table spinning for ever and leak
18404
+ * one entry per attempt. Same for the watchdog — the link can stay up
18405
+ * and the answer still never land.
18406
+ ************************************************************/
18407
+ var PAGE_TIMEOUT_MS = 2e4;
18408
+ function request_page(gobj, page, size) {
18409
+ let priv = gobj.priv;
18410
+ return new Promise(function(resolve, reject) {
18411
+ if (!priv._pending_pages) priv._pending_pages = {};
18412
+ let req_id = `p${++priv._page_seq}`;
18413
+ let timer = window.setTimeout(function() {
18414
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_PAGE_TIMEOUT", { req_id }, gobj);
18415
+ }, PAGE_TIMEOUT_MS);
18416
+ priv._pending_pages[req_id] = {
18417
+ resolve,
18418
+ reject,
18419
+ timer
18420
+ };
18421
+ (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_REQUEST_PAGE", {
18422
+ topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
18423
+ req_id,
18424
+ from: (page - 1) * size + 1,
18425
+ limit: size
18426
+ });
18427
+ });
18428
+ }
18429
+ /************************************************************
18430
+ * Settle a parked page request, however it ended.
18431
+ ************************************************************/
18432
+ function settle_page(gobj, req_id, value, error) {
18433
+ let priv = gobj.priv;
18434
+ let pend = priv._pending_pages ? priv._pending_pages[req_id] : null;
18435
+ if (!pend) return;
18436
+ delete priv._pending_pages[req_id];
18437
+ if (pend.timer) window.clearTimeout(pend.timer);
18438
+ if (error) {
18439
+ pend.reject(new Error(error));
18440
+ return;
18441
+ }
18442
+ pend.resolve(value);
18443
+ }
18444
+ /************************************************************
18445
+ * Pull the current page again. `replaceData()` re-runs
18446
+ * ajaxRequestFunc for the page the reader is on, so a refresh keeps
18447
+ * their position instead of throwing them back to the first page.
18448
+ ************************************************************/
18449
+ function ac_repull_page(gobj, event, kw, src) {
18450
+ let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
18451
+ if (!tabulator) return 0;
18452
+ try {
18453
+ tabulator.replaceData();
18454
+ } catch (e) {
18455
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: cannot re-pull the page: ${e}`);
18456
+ return -1;
18457
+ }
18458
+ return 0;
18459
+ }
18460
+ /************************************************************
18461
+ * The host answered a page.
18462
+ ************************************************************/
18463
+ function ac_page_loaded(gobj, event, kw, src) {
18464
+ settle_page(gobj, kw.req_id, {
18465
+ data: (0, _yuneta_gobj_js.is_array)(kw.rows) ? kw.rows : [],
18466
+ last_page: kw.pages || 1,
18467
+ last_row: typeof kw.total === "number" ? kw.total : 0
18468
+ }, null);
18469
+ return 0;
18470
+ }
18471
+ /************************************************************
18472
+ * The host could not answer it.
18473
+ ************************************************************/
18474
+ function ac_page_failed(gobj, event, kw, src) {
18475
+ let why = kw && kw.error || "page request failed";
18476
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${why}`);
18477
+ settle_page(gobj, kw.req_id, null, why);
18478
+ return 0;
18479
+ }
18480
+ /************************************************************
18481
+ * Nobody answered in time.
18482
+ ************************************************************/
18483
+ function ac_page_timeout(gobj, event, kw, src) {
18484
+ (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`);
18485
+ settle_page(gobj, kw.req_id, null, "page request timed out");
18486
+ return 0;
18487
+ }
18488
+ /************************************************************
18315
18489
  * Filter the loaded rows by a term matched against every non-internal
18316
18490
  * field. `clearFilter()` with no argument drops only THIS filter: the
18317
18491
  * per-column header filters are a separate layer and survive, so a
@@ -18559,6 +18733,26 @@ function create_gclass$5(gclass_name) {
18559
18733
  ac_cell_edited,
18560
18734
  null
18561
18735
  ],
18736
+ [
18737
+ "EV_REPULL_PAGE",
18738
+ ac_repull_page,
18739
+ null
18740
+ ],
18741
+ [
18742
+ "EV_PAGE_LOADED",
18743
+ ac_page_loaded,
18744
+ null
18745
+ ],
18746
+ [
18747
+ "EV_PAGE_FAILED",
18748
+ ac_page_failed,
18749
+ null
18750
+ ],
18751
+ [
18752
+ "EV_PAGE_TIMEOUT",
18753
+ ac_page_timeout,
18754
+ null
18755
+ ],
18562
18756
  [
18563
18757
  "EV_SEARCH",
18564
18758
  ac_search,
@@ -18610,7 +18804,12 @@ function create_gclass$5(gclass_name) {
18610
18804
  ["EV_REFRESH", 0],
18611
18805
  ["EV_SHOW_SCHEMA", 0],
18612
18806
  ["EV_CELL_EDITED", 0],
18807
+ ["EV_REPULL_PAGE", 0],
18808
+ ["EV_PAGE_LOADED", 0],
18809
+ ["EV_PAGE_FAILED", 0],
18810
+ ["EV_PAGE_TIMEOUT", 0],
18613
18811
  ["EV_UPDATE_FIELD", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
18812
+ ["EV_REQUEST_PAGE", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
18614
18813
  ["EV_SEARCH", 0],
18615
18814
  ["EV_OPEN_COLUMNS", 0],
18616
18815
  ["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,16 @@ 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 md = kw_get_dict(gobj, kw_command, "__md_command__", {}, 0);
15702
+ let req_id = kw_get_str(gobj, md, "req_id", "", 0);
15703
+ if (req_id) gobj_send_event(gobj_topic_form, "EV_PAGE_LOADED", {
15704
+ req_id,
15705
+ rows: answer.rows,
15706
+ pages: answer.pages,
15707
+ total: answer.total
15708
+ }, gobj);
15709
+ else gobj_send_event(gobj_topic_form, "EV_LOAD_NODES", answer.rows, gobj);
15688
15710
  }
15689
15711
  break;
15690
15712
  case "create-node":
@@ -15910,6 +15932,31 @@ function ac_create_record(gobj, event, kw, src) {
15910
15932
  });
15911
15933
  }
15912
15934
  /********************************************
15935
+ * A topic table wants a page.
15936
+ *
15937
+ * The transport is ours, so the table asks and we fetch; the answer
15938
+ * finds its way back by the `req_id` we echo. A backend that cannot
15939
+ * page answers the whole list, which the table takes as one page —
15940
+ * which is the truth, and is why the table can ask without knowing
15941
+ * what it is talking to.
15942
+ ********************************************/
15943
+ function ac_request_page(gobj, event, kw, src) {
15944
+ let topic_name = kw.topic_name || gobj_read_attr(src, "topic_name");
15945
+ if (!topic_name || !kw.req_id) {
15946
+ log_error(`${gobj_short_name(gobj)}: a page request with no topic or no id`);
15947
+ return -1;
15948
+ }
15949
+ if (treedb_nodes$1(gobj, gobj_read_str_attr(gobj, "treedb_name"), topic_name, { list_dict: true }, {
15950
+ from: kw.from,
15951
+ limit: kw.limit,
15952
+ req_id: kw.req_id
15953
+ }) < 0) gobj_send_event(src, "EV_PAGE_FAILED", {
15954
+ req_id: kw.req_id,
15955
+ error: "cannot reach the backend"
15956
+ }, gobj);
15957
+ return 0;
15958
+ }
15959
+ /********************************************
15913
15960
  * Event from formtable: ONE field of one record, edited in place.
15914
15961
  *
15915
15962
  * Written as a PARTIAL update and, deliberately, with NO `autolink`.
@@ -15976,6 +16023,11 @@ function ac_delete_record(gobj, event, kw, src) {
15976
16023
  * }
15977
16024
  ********************************************/
15978
16025
  function ac_refresh_topic(gobj, event, kw, src) {
16026
+ let gobj_topic_form = get_gobj_formtable(gobj, kw.topic_name);
16027
+ if (gobj_topic_form && gobj_read_bool_attr(gobj_topic_form, "with_remote_paging")) {
16028
+ gobj_send_event(gobj_topic_form, "EV_REPULL_PAGE", {}, gobj);
16029
+ return 0;
16030
+ }
15979
16031
  treedb_nodes$1(gobj, gobj_read_str_attr(gobj, "treedb_name"), kw.topic_name, { list_dict: true });
15980
16032
  return 0;
15981
16033
  }
@@ -16065,6 +16117,11 @@ function create_gclass$6(gclass_name) {
16065
16117
  ac_update_field,
16066
16118
  null
16067
16119
  ],
16120
+ [
16121
+ "EV_REQUEST_PAGE",
16122
+ ac_request_page,
16123
+ null
16124
+ ],
16068
16125
  [
16069
16126
  "EV_DELETE_RECORD",
16070
16127
  ac_delete_record,
@@ -16139,6 +16196,7 @@ function create_gclass$6(gclass_name) {
16139
16196
  ["EV_CREATE_RECORD", 0],
16140
16197
  ["EV_UPDATE_RECORD", 0],
16141
16198
  ["EV_UPDATE_FIELD", 0],
16199
+ ["EV_REQUEST_PAGE", 0],
16142
16200
  ["EV_DELETE_RECORD", 0],
16143
16201
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
16144
16202
  ["EV_REFRESH_TOPIC", 0],
@@ -16353,6 +16411,8 @@ var attrs_table$5 = [
16353
16411
  SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
16354
16412
  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
16413
  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)"),
16414
+ 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"),
16415
+ 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
16416
  SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
16357
16417
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16358
16418
  SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
@@ -16381,6 +16441,8 @@ var attrs_table$5 = [
16381
16441
  SDATA_END()
16382
16442
  ];
16383
16443
  var PRIVATE_DATA$5 = {
16444
+ _pending_pages: null,
16445
+ _page_seq: 0,
16384
16446
  $container: null,
16385
16447
  treedb_name: "",
16386
16448
  topic_name: "",
@@ -17106,6 +17168,25 @@ function create_tabulator(gobj) {
17106
17168
  let pkey = desc.pkey || "id";
17107
17169
  let selectable = with_checkbox ? "highlight" : with_radio ? 1 : false;
17108
17170
  let tabulator_settings = json_deep_copy(gobj_read_attr(gobj, "tabulator_settings"));
17171
+ if (gobj_read_bool_attr(gobj, "with_remote_paging")) {
17172
+ let page_size = gobj_read_integer_attr(gobj, "page_size") || 200;
17173
+ Object.assign(tabulator_settings, {
17174
+ pagination: true,
17175
+ paginationMode: "remote",
17176
+ filterMode: "local",
17177
+ paginationSize: page_size,
17178
+ paginationSizeSelector: [
17179
+ 50,
17180
+ 100,
17181
+ 200,
17182
+ 500
17183
+ ],
17184
+ ajaxURL: "nodes",
17185
+ ajaxRequestFunc: function(url, config, params) {
17186
+ return request_page(gobj, params.page || 1, params.size || page_size);
17187
+ }
17188
+ });
17189
+ }
17109
17190
  Object.assign(tabulator_settings, {
17110
17191
  index: pkey,
17111
17192
  columns,
@@ -18307,6 +18388,99 @@ function ac_show_schema(gobj, event, kw, src) {
18307
18388
  return 0;
18308
18389
  }
18309
18390
  /************************************************************
18391
+ * Ask the host for one page and park the promise Tabulator wants.
18392
+ *
18393
+ * The transport belongs to the HOST, so the request goes up as an event
18394
+ * and the answer comes back down as one; what lives here is only the
18395
+ * promise, keyed by a correlation id the host echoes.
18396
+ *
18397
+ * Rejected right away when there is nobody to ask: a request that can
18398
+ * never be answered would leave the table spinning for ever and leak
18399
+ * one entry per attempt. Same for the watchdog — the link can stay up
18400
+ * and the answer still never land.
18401
+ ************************************************************/
18402
+ var PAGE_TIMEOUT_MS = 2e4;
18403
+ function request_page(gobj, page, size) {
18404
+ let priv = gobj.priv;
18405
+ return new Promise(function(resolve, reject) {
18406
+ if (!priv._pending_pages) priv._pending_pages = {};
18407
+ let req_id = `p${++priv._page_seq}`;
18408
+ let timer = window.setTimeout(function() {
18409
+ gobj_send_event(gobj, "EV_PAGE_TIMEOUT", { req_id }, gobj);
18410
+ }, PAGE_TIMEOUT_MS);
18411
+ priv._pending_pages[req_id] = {
18412
+ resolve,
18413
+ reject,
18414
+ timer
18415
+ };
18416
+ gobj_publish_event(gobj, "EV_REQUEST_PAGE", {
18417
+ topic_name: gobj_read_str_attr(gobj, "topic_name"),
18418
+ req_id,
18419
+ from: (page - 1) * size + 1,
18420
+ limit: size
18421
+ });
18422
+ });
18423
+ }
18424
+ /************************************************************
18425
+ * Settle a parked page request, however it ended.
18426
+ ************************************************************/
18427
+ function settle_page(gobj, req_id, value, error) {
18428
+ let priv = gobj.priv;
18429
+ let pend = priv._pending_pages ? priv._pending_pages[req_id] : null;
18430
+ if (!pend) return;
18431
+ delete priv._pending_pages[req_id];
18432
+ if (pend.timer) window.clearTimeout(pend.timer);
18433
+ if (error) {
18434
+ pend.reject(new Error(error));
18435
+ return;
18436
+ }
18437
+ pend.resolve(value);
18438
+ }
18439
+ /************************************************************
18440
+ * Pull the current page again. `replaceData()` re-runs
18441
+ * ajaxRequestFunc for the page the reader is on, so a refresh keeps
18442
+ * their position instead of throwing them back to the first page.
18443
+ ************************************************************/
18444
+ function ac_repull_page(gobj, event, kw, src) {
18445
+ let tabulator = gobj_read_attr(gobj, "tabulator");
18446
+ if (!tabulator) return 0;
18447
+ try {
18448
+ tabulator.replaceData();
18449
+ } catch (e) {
18450
+ log_error(`${gobj_short_name(gobj)}: cannot re-pull the page: ${e}`);
18451
+ return -1;
18452
+ }
18453
+ return 0;
18454
+ }
18455
+ /************************************************************
18456
+ * The host answered a page.
18457
+ ************************************************************/
18458
+ function ac_page_loaded(gobj, event, kw, src) {
18459
+ settle_page(gobj, kw.req_id, {
18460
+ data: is_array(kw.rows) ? kw.rows : [],
18461
+ last_page: kw.pages || 1,
18462
+ last_row: typeof kw.total === "number" ? kw.total : 0
18463
+ }, null);
18464
+ return 0;
18465
+ }
18466
+ /************************************************************
18467
+ * The host could not answer it.
18468
+ ************************************************************/
18469
+ function ac_page_failed(gobj, event, kw, src) {
18470
+ let why = kw && kw.error || "page request failed";
18471
+ log_error(`${gobj_short_name(gobj)}: ${why}`);
18472
+ settle_page(gobj, kw.req_id, null, why);
18473
+ return 0;
18474
+ }
18475
+ /************************************************************
18476
+ * Nobody answered in time.
18477
+ ************************************************************/
18478
+ function ac_page_timeout(gobj, event, kw, src) {
18479
+ log_error(`${gobj_short_name(gobj)}: no answer for page request '${kw.req_id}' in ${PAGE_TIMEOUT_MS}ms`);
18480
+ settle_page(gobj, kw.req_id, null, "page request timed out");
18481
+ return 0;
18482
+ }
18483
+ /************************************************************
18310
18484
  * Filter the loaded rows by a term matched against every non-internal
18311
18485
  * field. `clearFilter()` with no argument drops only THIS filter: the
18312
18486
  * per-column header filters are a separate layer and survive, so a
@@ -18554,6 +18728,26 @@ function create_gclass$5(gclass_name) {
18554
18728
  ac_cell_edited,
18555
18729
  null
18556
18730
  ],
18731
+ [
18732
+ "EV_REPULL_PAGE",
18733
+ ac_repull_page,
18734
+ null
18735
+ ],
18736
+ [
18737
+ "EV_PAGE_LOADED",
18738
+ ac_page_loaded,
18739
+ null
18740
+ ],
18741
+ [
18742
+ "EV_PAGE_FAILED",
18743
+ ac_page_failed,
18744
+ null
18745
+ ],
18746
+ [
18747
+ "EV_PAGE_TIMEOUT",
18748
+ ac_page_timeout,
18749
+ null
18750
+ ],
18557
18751
  [
18558
18752
  "EV_SEARCH",
18559
18753
  ac_search,
@@ -18605,7 +18799,12 @@ function create_gclass$5(gclass_name) {
18605
18799
  ["EV_REFRESH", 0],
18606
18800
  ["EV_SHOW_SCHEMA", 0],
18607
18801
  ["EV_CELL_EDITED", 0],
18802
+ ["EV_REPULL_PAGE", 0],
18803
+ ["EV_PAGE_LOADED", 0],
18804
+ ["EV_PAGE_FAILED", 0],
18805
+ ["EV_PAGE_TIMEOUT", 0],
18608
18806
  ["EV_UPDATE_FIELD", event_flag_t.EVF_OUTPUT_EVENT],
18807
+ ["EV_REQUEST_PAGE", event_flag_t.EVF_OUTPUT_EVENT],
18609
18808
  ["EV_SEARCH", 0],
18610
18809
  ["EV_OPEN_COLUMNS", 0],
18611
18810
  ["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.0",
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,36 @@ 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 travels in `__md_command__` of the kw
1807
+ * that was SENT — the same place `topic_name` rides — so it
1808
+ * is read back from the command stack and never sent to the
1809
+ * backend as a parameter it does not know. */
1810
+ let md = kw_get_dict(gobj, kw_command, "__md_command__", {}, 0);
1811
+ let req_id = kw_get_str(gobj, md, "req_id", "", 0);
1812
+ if(req_id) {
1813
+ gobj_send_event(
1814
+ gobj_topic_form,
1815
+ "EV_PAGE_LOADED",
1816
+ {
1817
+ req_id: req_id,
1818
+ rows: answer.rows,
1819
+ pages: answer.pages,
1820
+ total: answer.total
1821
+ },
1822
+ gobj
1823
+ );
1824
+ } else {
1825
+ gobj_send_event(
1826
+ gobj_topic_form,
1827
+ "EV_LOAD_NODES",
1828
+ answer.rows,
1829
+ gobj
1830
+ );
1831
+ }
1788
1832
  }
1789
1833
  break;
1790
1834
 
@@ -2147,6 +2191,40 @@ function ac_create_record(gobj, event, kw, src)
2147
2191
  );
2148
2192
  }
2149
2193
 
2194
+ /********************************************
2195
+ * A topic table wants a page.
2196
+ *
2197
+ * The transport is ours, so the table asks and we fetch; the answer
2198
+ * finds its way back by the `req_id` we echo. A backend that cannot
2199
+ * page answers the whole list, which the table takes as one page —
2200
+ * which is the truth, and is why the table can ask without knowing
2201
+ * what it is talking to.
2202
+ ********************************************/
2203
+ function ac_request_page(gobj, event, kw, src)
2204
+ {
2205
+ let topic_name = kw.topic_name || gobj_read_attr(src, "topic_name");
2206
+ if(!topic_name || !kw.req_id) {
2207
+ log_error(`${gobj_short_name(gobj)}: a page request with no topic or no id`);
2208
+ return -1;
2209
+ }
2210
+
2211
+ let ret = treedb_nodes(
2212
+ gobj,
2213
+ gobj_read_str_attr(gobj, "treedb_name"),
2214
+ topic_name,
2215
+ {list_dict: true},
2216
+ {from: kw.from, limit: kw.limit, req_id: kw.req_id}
2217
+ );
2218
+
2219
+ if(ret < 0) {
2220
+ /* The command never went out, so no answer will ever come: tell the
2221
+ * table now instead of leaving it spinning until its watchdog. */
2222
+ gobj_send_event(src, "EV_PAGE_FAILED",
2223
+ {req_id: kw.req_id, error: "cannot reach the backend"}, gobj);
2224
+ }
2225
+ return 0;
2226
+ }
2227
+
2150
2228
  /********************************************
2151
2229
  * Event from formtable: ONE field of one record, edited in place.
2152
2230
  *
@@ -2256,6 +2334,14 @@ function ac_delete_record(gobj, event, kw, src)
2256
2334
  ********************************************/
2257
2335
  function ac_refresh_topic(gobj, event, kw, src)
2258
2336
  {
2337
+ let gobj_topic_form = get_gobj_formtable(gobj, kw.topic_name);
2338
+ if(gobj_topic_form && gobj_read_bool_attr(gobj_topic_form, "with_remote_paging")) {
2339
+ /* A table that pulls refreshes by pulling: pushing a whole topic
2340
+ * into a paged table would replace its page with everything. */
2341
+ gobj_send_event(gobj_topic_form, "EV_REPULL_PAGE", {}, gobj);
2342
+ return 0;
2343
+ }
2344
+
2259
2345
  let options = {
2260
2346
  list_dict: true
2261
2347
  };
@@ -2357,6 +2443,7 @@ function create_gclass(gclass_name)
2357
2443
  ["EV_CREATE_RECORD", ac_create_record, null],
2358
2444
  ["EV_UPDATE_RECORD", ac_update_record, null],
2359
2445
  ["EV_UPDATE_FIELD", ac_update_field, null],
2446
+ ["EV_REQUEST_PAGE", ac_request_page, null],
2360
2447
  ["EV_DELETE_RECORD", ac_delete_record, null],
2361
2448
  ["EV_REFRESH_TOPIC", ac_refresh_topic, null],
2362
2449
  ["EV_OPEN_JSON", ac_open_json, null],
@@ -2384,6 +2471,7 @@ function create_gclass(gclass_name)
2384
2471
  ["EV_CREATE_RECORD", 0],
2385
2472
  ["EV_UPDATE_RECORD", 0],
2386
2473
  ["EV_UPDATE_FIELD", 0],
2474
+ ["EV_REQUEST_PAGE", 0],
2387
2475
  ["EV_DELETE_RECORD", 0],
2388
2476
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT|
2389
2477
  event_flag_t.EVF_NO_WARN_SUBS],