@yuneta/gobj-ui 7.8.0 → 7.8.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.
- package/README.md +33 -0
- package/dist/gobj-ui.cjs.js +61 -1
- package/dist/gobj-ui.es.js +61 -1
- package/package.json +1 -1
- package/src/c_yui_treedb_topics.js +10 -1
- package/src/nodes_answer.js +72 -0
- package/src/nodes_answer.test.js +50 -0
package/README.md
CHANGED
|
@@ -491,6 +491,39 @@ The buttons of that toolbar never shrink. When the row runs out of room, the
|
|
|
491
491
|
url is what gives way, cut with an ellipsis, and the whole value stays in the
|
|
492
492
|
`title` and the `aria-label`.
|
|
493
493
|
|
|
494
|
+
### Editing a topic table in place
|
|
495
|
+
|
|
496
|
+
A writable scalar is editable in the table, in edition mode
|
|
497
|
+
(`with_inline_edit`, default on). Changing one field used to mean opening the
|
|
498
|
+
record form, changing it, saving and closing.
|
|
499
|
+
|
|
500
|
+
**Which cells, and why not the rest.** The schema decides first: only a column
|
|
501
|
+
flagged `writable`, and never the pkey — renaming what a record is KEYED by is
|
|
502
|
+
not a field edit. Then the type: a hook holds children and an fkey IS a link,
|
|
503
|
+
so both are edited by linking; a dict or a list is a document the form has an
|
|
504
|
+
editor for; a date cell shows a formatted string over an epoch, so typing into
|
|
505
|
+
it would write the string. Those stay with the form, one click away on the
|
|
506
|
+
same row. `boolean` gets a tick, `enum` the list of its own values, numbers a
|
|
507
|
+
number editor.
|
|
508
|
+
|
|
509
|
+
**The write is a partial update with no `autolink`, and that is the whole
|
|
510
|
+
safety of it.** `treedb_update_node()` merges (`json_object_update`), so the
|
|
511
|
+
fields it does not carry are left alone; `autolink` is the option that wipes a
|
|
512
|
+
node's links and rebuilds them from the fkeys the record carries, and on a
|
|
513
|
+
partial record it reads that as "no parents", detaches the node and answers
|
|
514
|
+
**success**. So a cell edit travels as its own event, `EV_UPDATE_FIELD`, and
|
|
515
|
+
not as `EV_UPDATE_RECORD` — that one does send autolink, and may, because the
|
|
516
|
+
form hands it the whole record with its fkeys in it. See
|
|
517
|
+
`schema_write_options.js` for the rule and why each word of it is there.
|
|
518
|
+
|
|
519
|
+
`editable` is a **function** on the column, not a flag: edition mode is
|
|
520
|
+
toggled on a table that is already built, so the answer has to be asked for at
|
|
521
|
+
the moment of the click.
|
|
522
|
+
|
|
523
|
+
A refused write puts the topic back to what the treedb has. Leaving the typed
|
|
524
|
+
value on screen is tolerable for a form, which stays open on the values it
|
|
525
|
+
failed with; a cell edited in place would just look saved.
|
|
526
|
+
|
|
494
527
|
### Reading a topic table: filters, columns, CSV
|
|
495
528
|
|
|
496
529
|
`C_YUI_TREEDB_TOPIC_WITH_FORM` had one global search box over the loaded rows.
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -14416,6 +14416,66 @@ function yui_shell_show_route_map(shell, opts) {
|
|
|
14416
14416
|
return modal_ref.modal;
|
|
14417
14417
|
}
|
|
14418
14418
|
//#endregion
|
|
14419
|
+
//#region src/nodes_answer.js
|
|
14420
|
+
/***********************************************************************
|
|
14421
|
+
* nodes_answer.js
|
|
14422
|
+
*
|
|
14423
|
+
* What the `nodes` command answered, whatever shape it came in.
|
|
14424
|
+
*
|
|
14425
|
+
* A treedb lives in memory on the backend, so walking it is not
|
|
14426
|
+
* what costs: serializing every node, pushing it through a
|
|
14427
|
+
* websocket and parsing it here is. So `nodes` learned to answer a
|
|
14428
|
+
* PAGE, with the contract `list-keys` of C_TRANGER already used:
|
|
14429
|
+
*
|
|
14430
|
+
* no `limit` -> the plain list it has always answered
|
|
14431
|
+
* a `limit` -> {total_rows, pages, data}
|
|
14432
|
+
*
|
|
14433
|
+
* Both shapes are alive at once and will be for a long time: the
|
|
14434
|
+
* SPA talks to backends the operator configures, and an older one
|
|
14435
|
+
* answers the plain list whatever this app asks for. So the shape
|
|
14436
|
+
* is read, never assumed — a view that indexed `data` as an array
|
|
14437
|
+
* would render an object's keys as rows the day a backend
|
|
14438
|
+
* upgraded, which is the kind of break that looks like a bug in
|
|
14439
|
+
* the app.
|
|
14440
|
+
*
|
|
14441
|
+
* Pure and tested, because this is exactly the sort of thing that
|
|
14442
|
+
* cannot be exercised until the other side moves.
|
|
14443
|
+
*
|
|
14444
|
+
* Copyright (c) 2026, ArtGins.
|
|
14445
|
+
* All Rights Reserved.
|
|
14446
|
+
***********************************************************************/
|
|
14447
|
+
/***************************************************************
|
|
14448
|
+
* nodes_answer(data) -> {rows, total, pages, paged}
|
|
14449
|
+
*
|
|
14450
|
+
* `rows` is always an array, so a caller never has to ask which
|
|
14451
|
+
* shape it got. `paged` says whether the backend answered a page,
|
|
14452
|
+
* which is the one thing a caller may legitimately care about (it is
|
|
14453
|
+
* what tells a pager it has something to page).
|
|
14454
|
+
***************************************************************/
|
|
14455
|
+
function nodes_answer(data) {
|
|
14456
|
+
if (Array.isArray(data)) return {
|
|
14457
|
+
rows: data,
|
|
14458
|
+
total: data.length,
|
|
14459
|
+
pages: 1,
|
|
14460
|
+
paged: false
|
|
14461
|
+
};
|
|
14462
|
+
if (data && typeof data === "object" && Array.isArray(data.data)) {
|
|
14463
|
+
let rows = data.data;
|
|
14464
|
+
return {
|
|
14465
|
+
rows,
|
|
14466
|
+
total: typeof data.total_rows === "number" ? data.total_rows : rows.length,
|
|
14467
|
+
pages: typeof data.pages === "number" && data.pages > 0 ? data.pages : 1,
|
|
14468
|
+
paged: true
|
|
14469
|
+
};
|
|
14470
|
+
}
|
|
14471
|
+
return {
|
|
14472
|
+
rows: [],
|
|
14473
|
+
total: 0,
|
|
14474
|
+
pages: 1,
|
|
14475
|
+
paged: false
|
|
14476
|
+
};
|
|
14477
|
+
}
|
|
14478
|
+
//#endregion
|
|
14419
14479
|
//#region src/c_yui_treedb_topics.js
|
|
14420
14480
|
/***********************************************************************
|
|
14421
14481
|
* c_yui_treedb_topics.js
|
|
@@ -15629,7 +15689,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
15629
15689
|
let topic_name = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", _yuneta_gobj_js.kw_flag_t.KW_REQUIRED);
|
|
15630
15690
|
if (result >= 0) {
|
|
15631
15691
|
let gobj_topic_form = (0, _yuneta_gobj_js.gobj_find_child)(gobj, { __gobj_name__: `${(0, _yuneta_gobj_js.gobj_name)(gobj)}?${topic_name}` });
|
|
15632
|
-
(0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_LOAD_NODES", data, gobj);
|
|
15692
|
+
(0, _yuneta_gobj_js.gobj_send_event)(gobj_topic_form, "EV_LOAD_NODES", nodes_answer(data).rows, gobj);
|
|
15633
15693
|
}
|
|
15634
15694
|
break;
|
|
15635
15695
|
case "create-node":
|
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
|
|
@@ -15624,7 +15684,7 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
|
|
|
15624
15684
|
let topic_name = kw_get_str(gobj, kw_command, "topic_name", "", kw_flag_t.KW_REQUIRED);
|
|
15625
15685
|
if (result >= 0) {
|
|
15626
15686
|
let gobj_topic_form = gobj_find_child(gobj, { __gobj_name__: `${gobj_name(gobj)}?${topic_name}` });
|
|
15627
|
-
gobj_send_event(gobj_topic_form, "EV_LOAD_NODES", data, gobj);
|
|
15687
|
+
gobj_send_event(gobj_topic_form, "EV_LOAD_NODES", nodes_answer(data).rows, gobj);
|
|
15628
15688
|
}
|
|
15629
15689
|
break;
|
|
15630
15690
|
case "create-node":
|
package/package.json
CHANGED
|
@@ -58,6 +58,7 @@ import "./c_yui_treedb_topics.css";
|
|
|
58
58
|
|
|
59
59
|
import {yui_shell_show_error, yui_shell_show_modal, yui_shell_popup_layer} from "./shell_modals.js";
|
|
60
60
|
import {yui_shell_of, yui_shell_set_sub_routes} from "./c_yui_shell.js";
|
|
61
|
+
import {nodes_answer} from "./nodes_answer.js";
|
|
61
62
|
|
|
62
63
|
import {t} from "i18next";
|
|
63
64
|
|
|
@@ -1770,10 +1771,18 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
1770
1771
|
let gobj_topic_form = gobj_find_child(gobj, {
|
|
1771
1772
|
__gobj_name__: `${gobj_name(gobj)}?${topic_name}`
|
|
1772
1773
|
});
|
|
1774
|
+
/* `nodes` answers a plain list, or — when asked for a page —
|
|
1775
|
+
* the {total_rows, pages, data} envelope. Both shapes are
|
|
1776
|
+
* alive at once and will be for a long time: this SPA talks
|
|
1777
|
+
* to backends the operator configures, and an older one
|
|
1778
|
+
* answers the plain list whatever is asked of it. Reading
|
|
1779
|
+
* the shape rather than assuming it is what keeps the table
|
|
1780
|
+
* from rendering an object's keys as rows the day a backend
|
|
1781
|
+
* upgrades. */
|
|
1773
1782
|
gobj_send_event(
|
|
1774
1783
|
gobj_topic_form,
|
|
1775
1784
|
"EV_LOAD_NODES",
|
|
1776
|
-
data,
|
|
1785
|
+
nodes_answer(data).rows,
|
|
1777
1786
|
gobj
|
|
1778
1787
|
);
|
|
1779
1788
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* nodes_answer.js
|
|
3
|
+
*
|
|
4
|
+
* What the `nodes` command answered, whatever shape it came in.
|
|
5
|
+
*
|
|
6
|
+
* A treedb lives in memory on the backend, so walking it is not
|
|
7
|
+
* what costs: serializing every node, pushing it through a
|
|
8
|
+
* websocket and parsing it here is. So `nodes` learned to answer a
|
|
9
|
+
* PAGE, with the contract `list-keys` of C_TRANGER already used:
|
|
10
|
+
*
|
|
11
|
+
* no `limit` -> the plain list it has always answered
|
|
12
|
+
* a `limit` -> {total_rows, pages, data}
|
|
13
|
+
*
|
|
14
|
+
* Both shapes are alive at once and will be for a long time: the
|
|
15
|
+
* SPA talks to backends the operator configures, and an older one
|
|
16
|
+
* answers the plain list whatever this app asks for. So the shape
|
|
17
|
+
* is read, never assumed — a view that indexed `data` as an array
|
|
18
|
+
* would render an object's keys as rows the day a backend
|
|
19
|
+
* upgraded, which is the kind of break that looks like a bug in
|
|
20
|
+
* the app.
|
|
21
|
+
*
|
|
22
|
+
* Pure and tested, because this is exactly the sort of thing that
|
|
23
|
+
* cannot be exercised until the other side moves.
|
|
24
|
+
*
|
|
25
|
+
* Copyright (c) 2026, ArtGins.
|
|
26
|
+
* All Rights Reserved.
|
|
27
|
+
***********************************************************************/
|
|
28
|
+
|
|
29
|
+
/***************************************************************
|
|
30
|
+
* nodes_answer(data) -> {rows, total, pages, paged}
|
|
31
|
+
*
|
|
32
|
+
* `rows` is always an array, so a caller never has to ask which
|
|
33
|
+
* shape it got. `paged` says whether the backend answered a page,
|
|
34
|
+
* which is the one thing a caller may legitimately care about (it is
|
|
35
|
+
* what tells a pager it has something to page).
|
|
36
|
+
***************************************************************/
|
|
37
|
+
function nodes_answer(data)
|
|
38
|
+
{
|
|
39
|
+
if(Array.isArray(data)) {
|
|
40
|
+
return {
|
|
41
|
+
rows: data,
|
|
42
|
+
total: data.length,
|
|
43
|
+
pages: 1,
|
|
44
|
+
paged: false
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if(data && typeof data === "object" && Array.isArray(data.data)) {
|
|
49
|
+
let rows = data.data;
|
|
50
|
+
let total = (typeof data.total_rows === "number")? data.total_rows : rows.length;
|
|
51
|
+
let pages = (typeof data.pages === "number" && data.pages > 0)? data.pages : 1;
|
|
52
|
+
return {
|
|
53
|
+
rows: rows,
|
|
54
|
+
total: total,
|
|
55
|
+
pages: pages,
|
|
56
|
+
paged: true
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Anything else — null, an error body, a shape nobody has seen — is
|
|
61
|
+
* no rows. The caller renders an empty table, which is honest, and
|
|
62
|
+
* the command answer's own `result` is what says something failed. */
|
|
63
|
+
return {
|
|
64
|
+
rows: [],
|
|
65
|
+
total: 0,
|
|
66
|
+
pages: 1,
|
|
67
|
+
paged: false
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
export {nodes_answer};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
import {nodes_answer} from "./nodes_answer.js";
|
|
3
|
+
|
|
4
|
+
describe("nodes_answer", () => {
|
|
5
|
+
it("reads the plain list an unpaged backend answers", () => {
|
|
6
|
+
let a = nodes_answer([{id: "a"}, {id: "b"}]);
|
|
7
|
+
expect(a.rows.length).toBe(2);
|
|
8
|
+
expect(a.total).toBe(2);
|
|
9
|
+
expect(a.pages).toBe(1);
|
|
10
|
+
expect(a.paged).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("reads the envelope a paged backend answers", () => {
|
|
14
|
+
let a = nodes_answer({total_rows: 120, pages: 3, data: [{id: "a"}]});
|
|
15
|
+
expect(a.rows.length).toBe(1);
|
|
16
|
+
expect(a.total).toBe(120);
|
|
17
|
+
expect(a.pages).toBe(3);
|
|
18
|
+
expect(a.paged).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("an empty page still reports the true total", () => {
|
|
22
|
+
let a = nodes_answer({total_rows: 120, pages: 3, data: []});
|
|
23
|
+
expect(a.rows).toEqual([]);
|
|
24
|
+
expect(a.total).toBe(120);
|
|
25
|
+
expect(a.paged).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("an empty list is not a page", () => {
|
|
29
|
+
let a = nodes_answer([]);
|
|
30
|
+
expect(a.rows).toEqual([]);
|
|
31
|
+
expect(a.total).toBe(0);
|
|
32
|
+
expect(a.paged).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("falls back to the row count when the envelope omits its totals", () => {
|
|
36
|
+
let a = nodes_answer({data: [{id: "a"}, {id: "b"}]});
|
|
37
|
+
expect(a.total).toBe(2);
|
|
38
|
+
expect(a.pages).toBe(1);
|
|
39
|
+
expect(a.paged).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("gives no rows for anything it does not recognise", () => {
|
|
43
|
+
for(let bad of [null, undefined, 0, "x", {}, {data: "nope"}]) {
|
|
44
|
+
let a = nodes_answer(bad);
|
|
45
|
+
expect(a.rows).toEqual([]);
|
|
46
|
+
expect(a.total).toBe(0);
|
|
47
|
+
expect(a.paged).toBe(false);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|