@yuneta/gobj-ui 7.8.0 → 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.
- package/README.md +33 -0
- package/dist/gobj-ui.cjs.js +264 -5
- package/dist/gobj-ui.es.js +264 -5
- package/package.json +1 -1
- package/src/c_yui_treedb_topic_with_form.js +163 -0
- package/src/c_yui_treedb_topics.js +108 -11
- package/src/nodes_answer.js +72 -0
- package/src/nodes_answer.test.js +50 -0
package/dist/gobj-ui.es.js
CHANGED
|
@@ -14411,6 +14411,66 @@ function yui_shell_show_route_map(shell, opts) {
|
|
|
14411
14411
|
return modal_ref.modal;
|
|
14412
14412
|
}
|
|
14413
14413
|
//#endregion
|
|
14414
|
+
//#region src/nodes_answer.js
|
|
14415
|
+
/***********************************************************************
|
|
14416
|
+
* nodes_answer.js
|
|
14417
|
+
*
|
|
14418
|
+
* What the `nodes` command answered, whatever shape it came in.
|
|
14419
|
+
*
|
|
14420
|
+
* A treedb lives in memory on the backend, so walking it is not
|
|
14421
|
+
* what costs: serializing every node, pushing it through a
|
|
14422
|
+
* websocket and parsing it here is. So `nodes` learned to answer a
|
|
14423
|
+
* PAGE, with the contract `list-keys` of C_TRANGER already used:
|
|
14424
|
+
*
|
|
14425
|
+
* no `limit` -> the plain list it has always answered
|
|
14426
|
+
* a `limit` -> {total_rows, pages, data}
|
|
14427
|
+
*
|
|
14428
|
+
* Both shapes are alive at once and will be for a long time: the
|
|
14429
|
+
* SPA talks to backends the operator configures, and an older one
|
|
14430
|
+
* answers the plain list whatever this app asks for. So the shape
|
|
14431
|
+
* is read, never assumed — a view that indexed `data` as an array
|
|
14432
|
+
* would render an object's keys as rows the day a backend
|
|
14433
|
+
* upgraded, which is the kind of break that looks like a bug in
|
|
14434
|
+
* the app.
|
|
14435
|
+
*
|
|
14436
|
+
* Pure and tested, because this is exactly the sort of thing that
|
|
14437
|
+
* cannot be exercised until the other side moves.
|
|
14438
|
+
*
|
|
14439
|
+
* Copyright (c) 2026, ArtGins.
|
|
14440
|
+
* All Rights Reserved.
|
|
14441
|
+
***********************************************************************/
|
|
14442
|
+
/***************************************************************
|
|
14443
|
+
* nodes_answer(data) -> {rows, total, pages, paged}
|
|
14444
|
+
*
|
|
14445
|
+
* `rows` is always an array, so a caller never has to ask which
|
|
14446
|
+
* shape it got. `paged` says whether the backend answered a page,
|
|
14447
|
+
* which is the one thing a caller may legitimately care about (it is
|
|
14448
|
+
* what tells a pager it has something to page).
|
|
14449
|
+
***************************************************************/
|
|
14450
|
+
function nodes_answer(data) {
|
|
14451
|
+
if (Array.isArray(data)) return {
|
|
14452
|
+
rows: data,
|
|
14453
|
+
total: data.length,
|
|
14454
|
+
pages: 1,
|
|
14455
|
+
paged: false
|
|
14456
|
+
};
|
|
14457
|
+
if (data && typeof data === "object" && Array.isArray(data.data)) {
|
|
14458
|
+
let rows = data.data;
|
|
14459
|
+
return {
|
|
14460
|
+
rows,
|
|
14461
|
+
total: typeof data.total_rows === "number" ? data.total_rows : rows.length,
|
|
14462
|
+
pages: typeof data.pages === "number" && data.pages > 0 ? data.pages : 1,
|
|
14463
|
+
paged: true
|
|
14464
|
+
};
|
|
14465
|
+
}
|
|
14466
|
+
return {
|
|
14467
|
+
rows: [],
|
|
14468
|
+
total: 0,
|
|
14469
|
+
pages: 1,
|
|
14470
|
+
paged: false
|
|
14471
|
+
};
|
|
14472
|
+
}
|
|
14473
|
+
//#endregion
|
|
14414
14474
|
//#region src/c_yui_treedb_topics.js
|
|
14415
14475
|
/***********************************************************************
|
|
14416
14476
|
* c_yui_treedb_topics.js
|
|
@@ -14432,6 +14492,7 @@ var attrs_table$6 = [
|
|
|
14432
14492
|
SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote yuno for data fetching"),
|
|
14433
14493
|
SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote TreeDB service name"),
|
|
14434
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"),
|
|
14435
14496
|
SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
|
|
14436
14497
|
SDATA(data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
|
|
14437
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)."),
|
|
@@ -15245,7 +15306,8 @@ function process_treedb_descs$1(gobj) {
|
|
|
15245
15306
|
with_delete_button: !readonly,
|
|
15246
15307
|
treedb_name,
|
|
15247
15308
|
topic_name: key,
|
|
15248
|
-
desc
|
|
15309
|
+
desc,
|
|
15310
|
+
with_remote_paging: gobj_read_bool_attr(gobj, "with_remote_paging")
|
|
15249
15311
|
};
|
|
15250
15312
|
let id = `${gobj_name(gobj)}?${desc.topic_name}`;
|
|
15251
15313
|
let gobj_topic_form = gobj_create_service(id, "C_YUI_TREEDB_TOPIC_WITH_FORM", kw_topic_form, gobj);
|
|
@@ -15316,6 +15378,8 @@ function register_sub_routes$1(gobj) {
|
|
|
15316
15378
|
function get_nodes$1(gobj, topic_name) {
|
|
15317
15379
|
const treedb_name = gobj.priv.treedb_name;
|
|
15318
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;
|
|
15319
15383
|
treedb_nodes$1(gobj, treedb_name, topic_name, { list_dict: true });
|
|
15320
15384
|
}
|
|
15321
15385
|
/************************************************************
|
|
@@ -15363,7 +15427,7 @@ function get_gobj_formtable(gobj, topic_name) {
|
|
|
15363
15427
|
/************************************************************
|
|
15364
15428
|
* Command to remote service
|
|
15365
15429
|
************************************************************/
|
|
15366
|
-
function treedb_nodes$1(gobj, treedb_name, topic_name, options) {
|
|
15430
|
+
function treedb_nodes$1(gobj, treedb_name, topic_name, options, page) {
|
|
15367
15431
|
let command = "nodes";
|
|
15368
15432
|
let gobj_remote_yuno = gobj_read_pointer_attr(gobj, "gobj_remote_yuno");
|
|
15369
15433
|
let kw = {
|
|
@@ -15373,8 +15437,17 @@ function treedb_nodes$1(gobj, treedb_name, topic_name, options) {
|
|
|
15373
15437
|
options: options || {}
|
|
15374
15438
|
};
|
|
15375
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
|
+
}
|
|
15376
15445
|
let ret = gobj_command(gobj_remote_yuno, command, kw, gobj);
|
|
15377
|
-
if (ret)
|
|
15446
|
+
if (ret) {
|
|
15447
|
+
log_error(ret);
|
|
15448
|
+
return -1;
|
|
15449
|
+
}
|
|
15450
|
+
return 0;
|
|
15378
15451
|
}
|
|
15379
15452
|
/************************************************************
|
|
15380
15453
|
* Command to remote service
|
|
@@ -15608,7 +15681,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
15608
15681
|
yui_shell_show_error(yui_shell_of(gobj), comment, { t });
|
|
15609
15682
|
if (command === "update-node" || command === "create-node") {
|
|
15610
15683
|
let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
|
|
15611
|
-
if (failed_topic)
|
|
15684
|
+
if (failed_topic) gobj_send_event(gobj, "EV_REFRESH_TOPIC", { topic_name: failed_topic }, gobj);
|
|
15612
15685
|
}
|
|
15613
15686
|
}
|
|
15614
15687
|
return 0;
|
|
@@ -15624,7 +15697,16 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
15624
15697
|
let topic_name = kw_get_str(gobj, kw_command, "topic_name", "", kw_flag_t.KW_REQUIRED);
|
|
15625
15698
|
if (result >= 0) {
|
|
15626
15699
|
let gobj_topic_form = gobj_find_child(gobj, { __gobj_name__: `${gobj_name(gobj)}?${topic_name}` });
|
|
15627
|
-
|
|
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);
|
|
15628
15710
|
}
|
|
15629
15711
|
break;
|
|
15630
15712
|
case "create-node":
|
|
@@ -15850,6 +15932,31 @@ function ac_create_record(gobj, event, kw, src) {
|
|
|
15850
15932
|
});
|
|
15851
15933
|
}
|
|
15852
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
|
+
/********************************************
|
|
15853
15960
|
* Event from formtable: ONE field of one record, edited in place.
|
|
15854
15961
|
*
|
|
15855
15962
|
* Written as a PARTIAL update and, deliberately, with NO `autolink`.
|
|
@@ -15916,6 +16023,11 @@ function ac_delete_record(gobj, event, kw, src) {
|
|
|
15916
16023
|
* }
|
|
15917
16024
|
********************************************/
|
|
15918
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
|
+
}
|
|
15919
16031
|
treedb_nodes$1(gobj, gobj_read_str_attr(gobj, "treedb_name"), kw.topic_name, { list_dict: true });
|
|
15920
16032
|
return 0;
|
|
15921
16033
|
}
|
|
@@ -16005,6 +16117,11 @@ function create_gclass$6(gclass_name) {
|
|
|
16005
16117
|
ac_update_field,
|
|
16006
16118
|
null
|
|
16007
16119
|
],
|
|
16120
|
+
[
|
|
16121
|
+
"EV_REQUEST_PAGE",
|
|
16122
|
+
ac_request_page,
|
|
16123
|
+
null
|
|
16124
|
+
],
|
|
16008
16125
|
[
|
|
16009
16126
|
"EV_DELETE_RECORD",
|
|
16010
16127
|
ac_delete_record,
|
|
@@ -16079,6 +16196,7 @@ function create_gclass$6(gclass_name) {
|
|
|
16079
16196
|
["EV_CREATE_RECORD", 0],
|
|
16080
16197
|
["EV_UPDATE_RECORD", 0],
|
|
16081
16198
|
["EV_UPDATE_FIELD", 0],
|
|
16199
|
+
["EV_REQUEST_PAGE", 0],
|
|
16082
16200
|
["EV_DELETE_RECORD", 0],
|
|
16083
16201
|
["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
16084
16202
|
["EV_REFRESH_TOPIC", 0],
|
|
@@ -16293,6 +16411,8 @@ var attrs_table$5 = [
|
|
|
16293
16411
|
SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
|
|
16294
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)"),
|
|
16295
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"),
|
|
16296
16416
|
SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
|
|
16297
16417
|
SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
|
|
16298
16418
|
SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
|
|
@@ -16321,6 +16441,8 @@ var attrs_table$5 = [
|
|
|
16321
16441
|
SDATA_END()
|
|
16322
16442
|
];
|
|
16323
16443
|
var PRIVATE_DATA$5 = {
|
|
16444
|
+
_pending_pages: null,
|
|
16445
|
+
_page_seq: 0,
|
|
16324
16446
|
$container: null,
|
|
16325
16447
|
treedb_name: "",
|
|
16326
16448
|
topic_name: "",
|
|
@@ -17046,6 +17168,25 @@ function create_tabulator(gobj) {
|
|
|
17046
17168
|
let pkey = desc.pkey || "id";
|
|
17047
17169
|
let selectable = with_checkbox ? "highlight" : with_radio ? 1 : false;
|
|
17048
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
|
+
}
|
|
17049
17190
|
Object.assign(tabulator_settings, {
|
|
17050
17191
|
index: pkey,
|
|
17051
17192
|
columns,
|
|
@@ -18247,6 +18388,99 @@ function ac_show_schema(gobj, event, kw, src) {
|
|
|
18247
18388
|
return 0;
|
|
18248
18389
|
}
|
|
18249
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
|
+
/************************************************************
|
|
18250
18484
|
* Filter the loaded rows by a term matched against every non-internal
|
|
18251
18485
|
* field. `clearFilter()` with no argument drops only THIS filter: the
|
|
18252
18486
|
* per-column header filters are a separate layer and survive, so a
|
|
@@ -18494,6 +18728,26 @@ function create_gclass$5(gclass_name) {
|
|
|
18494
18728
|
ac_cell_edited,
|
|
18495
18729
|
null
|
|
18496
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
|
+
],
|
|
18497
18751
|
[
|
|
18498
18752
|
"EV_SEARCH",
|
|
18499
18753
|
ac_search,
|
|
@@ -18545,7 +18799,12 @@ function create_gclass$5(gclass_name) {
|
|
|
18545
18799
|
["EV_REFRESH", 0],
|
|
18546
18800
|
["EV_SHOW_SCHEMA", 0],
|
|
18547
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],
|
|
18548
18806
|
["EV_UPDATE_FIELD", event_flag_t.EVF_OUTPUT_EVENT],
|
|
18807
|
+
["EV_REQUEST_PAGE", event_flag_t.EVF_OUTPUT_EVENT],
|
|
18549
18808
|
["EV_SEARCH", 0],
|
|
18550
18809
|
["EV_OPEN_COLUMNS", 0],
|
|
18551
18810
|
["EV_TOGGLE_COLUMN", 0],
|
package/package.json
CHANGED
|
@@ -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],
|