@yuneta/gobj-ui 5.14.2 → 5.15.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 +32 -0
- package/dist/gobj-ui.cjs.js +126 -8
- package/dist/gobj-ui.es.js +126 -8
- package/package.json +1 -1
- package/src/c_yui_treedb_topic_with_form.js +71 -6
- package/src/c_yui_treedb_topics.js +37 -4
- package/src/treedb_write_plan.js +73 -0
- package/src/treedb_write_plan.test.js +83 -0
package/README.md
CHANGED
|
@@ -430,6 +430,38 @@ Logical DOM classes: `JSON_VIEWER`, `JSON_TOOLBAR`, `JSON_SEARCH`, `JSON_TREE`,
|
|
|
430
430
|
`JSON_ROW`, `JSON_KEY`, `JSON_VALUE`, `JSON_SUMMARY`, `JSON_COLLAPSED`,
|
|
431
431
|
`JSON_TIME`. The gclass imports its own `c_yui_json.css`.
|
|
432
432
|
|
|
433
|
+
### Read-only treedbs: `readonly`
|
|
434
|
+
|
|
435
|
+
`C_YUI_TREEDB_TOPICS` takes a **`readonly`** attr, propagated to every topic it
|
|
436
|
+
builds. It is not one more button flag: it is the STATE of the treedb and it
|
|
437
|
+
beats each `with_*` flag at once, because a treedb whose tranger the yuno does
|
|
438
|
+
not master answers **every** write with
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
ERROR -1: <yuno>: treedb '<name>' is READ-ONLY, this yuno is not the master of its tranger
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
(the yuno refuses since SDK 7.13.0), so offering the buttons anyway turns a
|
|
445
|
+
fact into an error message per click. Ask the yuno which it is with
|
|
446
|
+
`command-yuno id=<yuno> service=<treedb> command=treedb-info`, which answers
|
|
447
|
+
`{treedb_name, master, schema_version, topics}` — and remember the flag is per
|
|
448
|
+
TREEDB and is runtime state: a yuno is routinely the master of its
|
|
449
|
+
`treedb_system_schema` and a replica of a data treedb it shares.
|
|
450
|
+
|
|
451
|
+
What `readonly` takes away: the edition mode, the *new* / *delete* / *paste*
|
|
452
|
+
buttons, the in-row edit icons, and the write half of the record form's toolbar
|
|
453
|
+
(`copy` stays — reading a record includes taking it with you) with the cells not
|
|
454
|
+
editable. The record form still OPENS: looking is the point of a replica.
|
|
455
|
+
|
|
456
|
+
Two implementation notes worth keeping:
|
|
457
|
+
|
|
458
|
+
- the decision lives in **`treedb_write_plan.js`** (pure, tested), not in five
|
|
459
|
+
`!readonly && with_x` expressions — five places to forget the sixth;
|
|
460
|
+
- and the write **events** are refused as well, in both gclasses, with a
|
|
461
|
+
`log_error`. Hiding a button is not the same as refusing a write: an event can
|
|
462
|
+
still arrive from a keyboard path or a form that outlived the flag, and an
|
|
463
|
+
ignored write is exactly the behaviour this whole change exists to stop.
|
|
464
|
+
|
|
433
465
|
### C_YUI_TREEDB_SCHEMA — the treedb as a graph of topics (prototype)
|
|
434
466
|
|
|
435
467
|
A landing view that draws a treedb as a **graph of topics** — one node per
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -14404,6 +14404,7 @@ var attrs_table$5 = [
|
|
|
14404
14404
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
|
|
14405
14405
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
|
|
14406
14406
|
(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)."),
|
|
14407
|
+
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "readonly", 0, false, "The whole treedb is read-only: every topic opens without its write affordances and the write events are refused. Set it when this yuno is not the MASTER of the treedb's tranger -- only the master can write, and the yuno answers 'READ-ONLY' to a write since SDK 7.13.0 (ask `treedb-info`)"),
|
|
14407
14408
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "card_action_routes", 0, null, "Per-card hash-route templates {info, table, graph} with a {topic} placeholder (host-supplied, route-agnostic). Present ⇒ cards show 3 icon actions; absent ⇒ a single card that opens the table."),
|
|
14408
14409
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_JSON, "landing_routes", 0, null, "Host-supplied hashes for the two landing sub-views {cards, schema}; the toggle navigates to them so the landing is URL-addressable (ROUTING.md). Absent ⇒ toggle flips in-view only (legacy)."),
|
|
14409
14410
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "base_route", 0, "", "This view's base route (host-supplied); used to declare its sub-routes (topics / info / schema) to the site map (ROUTING.md contributor)."),
|
|
@@ -15184,12 +15185,14 @@ function process_treedb_descs$1(gobj) {
|
|
|
15184
15185
|
let descs = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "descs");
|
|
15185
15186
|
let system = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "system");
|
|
15186
15187
|
let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
|
|
15188
|
+
let readonly = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly");
|
|
15187
15189
|
for (const [key, desc] of Object.entries(descs)) {
|
|
15188
15190
|
if (system) {} else if (key.substring(0, 2) === "__") continue;
|
|
15189
15191
|
let kw_topic_form = {
|
|
15190
15192
|
subscriber: gobj,
|
|
15191
|
-
|
|
15192
|
-
|
|
15193
|
+
readonly,
|
|
15194
|
+
with_new_button: !readonly,
|
|
15195
|
+
with_delete_button: !readonly,
|
|
15193
15196
|
treedb_name,
|
|
15194
15197
|
topic_name: key,
|
|
15195
15198
|
desc
|
|
@@ -15494,6 +15497,19 @@ function request_print_tranger$1(gobj, path) {
|
|
|
15494
15497
|
}, gobj);
|
|
15495
15498
|
if (ret) (0, _yuneta_gobj_js.log_error)(ret);
|
|
15496
15499
|
}
|
|
15500
|
+
/***************************************************************
|
|
15501
|
+
* The last gate before a write leaves for the yuno. The per-topic
|
|
15502
|
+
* children already hide their write buttons when the treedb is
|
|
15503
|
+
* read-only, but the record events converge HERE, and this is the
|
|
15504
|
+
* gclass that owns the treedb: a write that got this far on a replica
|
|
15505
|
+
* would travel, be refused by the yuno, and come back as a toast --
|
|
15506
|
+
* after the table had already drawn the row.
|
|
15507
|
+
***************************************************************/
|
|
15508
|
+
function refuse_if_readonly$1(gobj, event) {
|
|
15509
|
+
if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly")) return false;
|
|
15510
|
+
(0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} refused, treedb '${(0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name")}' is READ-ONLY`);
|
|
15511
|
+
return true;
|
|
15512
|
+
}
|
|
15497
15513
|
/***************************
|
|
15498
15514
|
* Actions
|
|
15499
15515
|
***************************/
|
|
@@ -15766,6 +15782,7 @@ function ac_treedb_node_deleted$1(gobj, event, kw, src) {
|
|
|
15766
15782
|
* }
|
|
15767
15783
|
********************************************/
|
|
15768
15784
|
function ac_create_record(gobj, event, kw, src) {
|
|
15785
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
15769
15786
|
let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
|
|
15770
15787
|
let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
|
|
15771
15788
|
let record = kw.record;
|
|
@@ -15783,6 +15800,7 @@ function ac_create_record(gobj, event, kw, src) {
|
|
|
15783
15800
|
* }
|
|
15784
15801
|
********************************************/
|
|
15785
15802
|
function ac_update_record(gobj, event, kw, src) {
|
|
15803
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
15786
15804
|
let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
|
|
15787
15805
|
let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
|
|
15788
15806
|
let record = kw.record;
|
|
@@ -15799,6 +15817,7 @@ function ac_update_record(gobj, event, kw, src) {
|
|
|
15799
15817
|
* }
|
|
15800
15818
|
********************************************/
|
|
15801
15819
|
function ac_delete_record(gobj, event, kw, src) {
|
|
15820
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
15802
15821
|
let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
|
|
15803
15822
|
let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
|
|
15804
15823
|
let record = kw.record;
|
|
@@ -16086,6 +16105,68 @@ function yui_tabulator_relocalize(table, t) {
|
|
|
16086
16105
|
}
|
|
16087
16106
|
}
|
|
16088
16107
|
//#endregion
|
|
16108
|
+
//#region src/treedb_write_plan.js
|
|
16109
|
+
/***********************************************************************
|
|
16110
|
+
* treedb_write_plan.js
|
|
16111
|
+
*
|
|
16112
|
+
* Which write affordances a treedb topic offers.
|
|
16113
|
+
*
|
|
16114
|
+
* Pure, so it can be tested without a DOM: the gclass turns the
|
|
16115
|
+
* plan into buttons and columns, this decides the plan. Same split
|
|
16116
|
+
* as form_toolbar_plan.js.
|
|
16117
|
+
*
|
|
16118
|
+
* The rule it exists to state ONCE: `readonly` is not one more
|
|
16119
|
+
* button flag, it is the STATE of the topic and it wins over all of
|
|
16120
|
+
* them. A treedb whose tranger this yuno does not master answers
|
|
16121
|
+
* every write with "READ-ONLY" (the yuno refuses since SDK 7.13.0),
|
|
16122
|
+
* so offering the buttons anyway only turns a fact into an error
|
|
16123
|
+
* message per click. Written as `!readonly && with_x` in five
|
|
16124
|
+
* places it is five places to forget the sixth.
|
|
16125
|
+
*
|
|
16126
|
+
* Copyright (c) 2026, ArtGins.
|
|
16127
|
+
* All Rights Reserved.
|
|
16128
|
+
***********************************************************************/
|
|
16129
|
+
var READONLY_FORM_TOOLBAR = ["copy"];
|
|
16130
|
+
/***************************************************************
|
|
16131
|
+
* plan_treedb_writes(flags) -> plan
|
|
16132
|
+
*
|
|
16133
|
+
* flags {readonly, with_edition_mode, with_new_button,
|
|
16134
|
+
* with_delete_button, with_paste_button,
|
|
16135
|
+
* with_in_row_edit_icons}
|
|
16136
|
+
* Every with_* defaults to TRUE when absent, which is
|
|
16137
|
+
* what the gclass attrs default to: a caller that sets
|
|
16138
|
+
* nothing gets the toolbar this view always had.
|
|
16139
|
+
*
|
|
16140
|
+
* plan the same names without the `with_` prefix, plus
|
|
16141
|
+
* `form_toolbar`: the list to hand C_YUI_FORM, or null
|
|
16142
|
+
* to leave the form's own default alone.
|
|
16143
|
+
***************************************************************/
|
|
16144
|
+
function plan_treedb_writes(flags) {
|
|
16145
|
+
let f = flags || {};
|
|
16146
|
+
let readonly = !!f.readonly;
|
|
16147
|
+
let want = (name) => {
|
|
16148
|
+
return f[name] === void 0 ? true : !!f[name];
|
|
16149
|
+
};
|
|
16150
|
+
if (readonly) return {
|
|
16151
|
+
readonly: true,
|
|
16152
|
+
edition_mode: false,
|
|
16153
|
+
new_button: false,
|
|
16154
|
+
delete_button: false,
|
|
16155
|
+
paste_button: false,
|
|
16156
|
+
in_row_icons: false,
|
|
16157
|
+
form_toolbar: READONLY_FORM_TOOLBAR.slice()
|
|
16158
|
+
};
|
|
16159
|
+
return {
|
|
16160
|
+
readonly: false,
|
|
16161
|
+
edition_mode: want("with_edition_mode"),
|
|
16162
|
+
new_button: want("with_new_button"),
|
|
16163
|
+
delete_button: want("with_delete_button"),
|
|
16164
|
+
paste_button: want("with_paste_button"),
|
|
16165
|
+
in_row_icons: want("with_in_row_edit_icons"),
|
|
16166
|
+
form_toolbar: null
|
|
16167
|
+
};
|
|
16168
|
+
}
|
|
16169
|
+
//#endregion
|
|
16089
16170
|
//#region src/c_yui_treedb_topic_with_form.js
|
|
16090
16171
|
/***********************************************************************
|
|
16091
16172
|
* c_yui_treedb_topic_with_form.js
|
|
@@ -16107,6 +16188,7 @@ var attrs_table$4 = [
|
|
|
16107
16188
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote service treedb name"),
|
|
16108
16189
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "topic_name", 0, null, "Topic name"),
|
|
16109
16190
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "desc", 0, null, "Description of topics"),
|
|
16191
|
+
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "readonly", 0, false, "The topic cannot be written: no edition mode, no new/delete/paste, no in-row icons, and the record form opens without its save toolbar. Set it when the treedb is not the master of its tranger (only the master can write; the yuno refuses otherwise), or when the user lacks the authz"),
|
|
16110
16192
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_edition_mode", 0, true, "Enable EDITION mode showing EDIT Button toolbar"),
|
|
16111
16193
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_new_button", 0, true, "Button toolbar NEW"),
|
|
16112
16194
|
(0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_delete_button", 0, true, "Button toolbar DELETE"),
|
|
@@ -16269,11 +16351,12 @@ async function readClipboard(gobj) {
|
|
|
16269
16351
|
function build_ui$4(gobj) {
|
|
16270
16352
|
function create_table_toolbar() {
|
|
16271
16353
|
let $table_toolbar = [];
|
|
16272
|
-
let
|
|
16273
|
-
let
|
|
16274
|
-
let
|
|
16354
|
+
let plan = write_plan(gobj);
|
|
16355
|
+
let with_edition_mode = plan.edition_mode;
|
|
16356
|
+
let with_new_button = plan.new_button;
|
|
16357
|
+
let with_delete_button = plan.delete_button;
|
|
16275
16358
|
let with_copy_button = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_copy_button");
|
|
16276
|
-
let with_paste_button =
|
|
16359
|
+
let with_paste_button = plan.paste_button;
|
|
16277
16360
|
let toolbar_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "toolbar_id");
|
|
16278
16361
|
if (with_edition_mode) {
|
|
16279
16362
|
$table_toolbar = (0, _yuneta_gobj_js.createElement2)(["div", {
|
|
@@ -16716,7 +16799,7 @@ function create_tabulator(gobj) {
|
|
|
16716
16799
|
"</button>"
|
|
16717
16800
|
].join("");
|
|
16718
16801
|
}
|
|
16719
|
-
if ((
|
|
16802
|
+
if (write_plan(gobj).in_row_icons) columns.push({
|
|
16720
16803
|
field: "_operation",
|
|
16721
16804
|
title: "Op",
|
|
16722
16805
|
hozAlign: "center",
|
|
@@ -16995,15 +17078,17 @@ function open_form_dialog(gobj, mode, record) {
|
|
|
16995
17078
|
},
|
|
16996
17079
|
[]
|
|
16997
17080
|
]);
|
|
17081
|
+
let form_plan = write_plan(gobj);
|
|
16998
17082
|
let form = (0, _yuneta_gobj_js.gobj_create_pure_child)("form_" + (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj)), "C_YUI_FORM", {
|
|
16999
17083
|
template: build_form_template(gobj),
|
|
17000
17084
|
record,
|
|
17001
17085
|
fkey_options: build_fkey_options(gobj),
|
|
17002
17086
|
form_mode: mode,
|
|
17087
|
+
...form_plan.form_toolbar ? { toolbar: form_plan.form_toolbar } : {},
|
|
17003
17088
|
render_mode: "edit",
|
|
17004
17089
|
pkey: desc.pkey || "id",
|
|
17005
17090
|
topic_name,
|
|
17006
|
-
editable:
|
|
17091
|
+
editable: !form_plan.readonly,
|
|
17007
17092
|
$parent: $body
|
|
17008
17093
|
}, gobj);
|
|
17009
17094
|
priv.form = form;
|
|
@@ -17530,6 +17615,34 @@ function show_dropdown_popup_menu(gobj, x, y, items, callback) {
|
|
|
17530
17615
|
let $with_focus = $element.querySelector(".with-focus");
|
|
17531
17616
|
if ($with_focus) $with_focus.focus();
|
|
17532
17617
|
}
|
|
17618
|
+
/***************************************************************
|
|
17619
|
+
* A read-only topic refuses the write EVENTS too, not only their
|
|
17620
|
+
* buttons. The buttons are gone, but an event can still arrive: a
|
|
17621
|
+
* keyboard path, a hosted form that outlived the flag, a caller that
|
|
17622
|
+
* sends EV_SAVE_RECORD itself. Refusing here is what makes `readonly`
|
|
17623
|
+
* a state of the gclass rather than a way of drawing it -- and it
|
|
17624
|
+
* fails LOUDLY, because an ignored write is how the backend used to
|
|
17625
|
+
* behave and what this whole change exists to stop.
|
|
17626
|
+
***************************************************************/
|
|
17627
|
+
function write_plan(gobj) {
|
|
17628
|
+
return plan_treedb_writes({
|
|
17629
|
+
readonly: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly"),
|
|
17630
|
+
with_edition_mode: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_edition_mode"),
|
|
17631
|
+
with_new_button: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_new_button"),
|
|
17632
|
+
with_delete_button: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_delete_button"),
|
|
17633
|
+
with_paste_button: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_paste_button"),
|
|
17634
|
+
with_in_row_edit_icons: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_in_row_edit_icons")
|
|
17635
|
+
});
|
|
17636
|
+
}
|
|
17637
|
+
/***************************************************************
|
|
17638
|
+
* A read-only topic refuses the write EVENTS too, not only their
|
|
17639
|
+
* buttons.
|
|
17640
|
+
***************************************************************/
|
|
17641
|
+
function refuse_if_readonly(gobj, event) {
|
|
17642
|
+
if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly")) return false;
|
|
17643
|
+
(0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: ${event} refused, topic '${(0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name")}' is READ-ONLY`);
|
|
17644
|
+
return true;
|
|
17645
|
+
}
|
|
17533
17646
|
/***************************
|
|
17534
17647
|
* Actions
|
|
17535
17648
|
***************************/
|
|
@@ -17622,6 +17735,7 @@ function ac_node_deleted$1(gobj, event, kw, src) {
|
|
|
17622
17735
|
* and the Op column is visible (with row's edit/delete icons)
|
|
17623
17736
|
************************************************************/
|
|
17624
17737
|
function ac_edition_mode(gobj, event, kw, src) {
|
|
17738
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
17625
17739
|
let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
|
|
17626
17740
|
let editable = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable");
|
|
17627
17741
|
editable = !editable;
|
|
@@ -17665,6 +17779,7 @@ function ac_edition_mode(gobj, event, kw, src) {
|
|
|
17665
17779
|
* - the backend will broadcast the new record that will be added to table
|
|
17666
17780
|
************************************************************/
|
|
17667
17781
|
function ac_new_row(gobj, event, kw, src) {
|
|
17782
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
17668
17783
|
let row = {};
|
|
17669
17784
|
let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
|
|
17670
17785
|
for (let i = 0; i < desc.cols.length; i++) {
|
|
@@ -17694,6 +17809,7 @@ function ac_new_row(gobj, event, kw, src) {
|
|
|
17694
17809
|
* kw {index:, row:}
|
|
17695
17810
|
************************************************************/
|
|
17696
17811
|
function ac_delete_rows(gobj, event, kw, src) {
|
|
17812
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
17697
17813
|
let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
|
|
17698
17814
|
if ((0, _yuneta_gobj_js.json_size)(kw) === 0) {
|
|
17699
17815
|
let rows = tabulator.getSelectedData();
|
|
@@ -17757,6 +17873,7 @@ function ac_copy_rows(gobj, event, kw, src) {
|
|
|
17757
17873
|
* From internal
|
|
17758
17874
|
************************************************************/
|
|
17759
17875
|
function ac_paste_rows(gobj, event, kw, src) {
|
|
17876
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
17760
17877
|
if (kw.topic_name === (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name") && kw.treedb_name === (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name")) for (let row of kw.rows) (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_CREATE_RECORD", {
|
|
17761
17878
|
topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
|
|
17762
17879
|
record: row
|
|
@@ -17769,6 +17886,7 @@ function ac_paste_rows(gobj, event, kw, src) {
|
|
|
17769
17886
|
* fkeys, times and numbers). Route by the form's mode.
|
|
17770
17887
|
************************************************************/
|
|
17771
17888
|
function ac_form_save_record(gobj, event, kw, src) {
|
|
17889
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
17772
17890
|
let mode = (0, _yuneta_gobj_js.gobj_read_str_attr)(src, "form_mode");
|
|
17773
17891
|
(0, _yuneta_gobj_js.gobj_publish_event)(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", {
|
|
17774
17892
|
topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -14399,6 +14399,7 @@ var attrs_table$5 = [
|
|
|
14399
14399
|
SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
|
|
14400
14400
|
SDATA(data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
|
|
14401
14401
|
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)."),
|
|
14402
|
+
SDATA(data_type_t.DTP_BOOLEAN, "readonly", 0, false, "The whole treedb is read-only: every topic opens without its write affordances and the write events are refused. Set it when this yuno is not the MASTER of the treedb's tranger -- only the master can write, and the yuno answers 'READ-ONLY' to a write since SDK 7.13.0 (ask `treedb-info`)"),
|
|
14402
14403
|
SDATA(data_type_t.DTP_JSON, "card_action_routes", 0, null, "Per-card hash-route templates {info, table, graph} with a {topic} placeholder (host-supplied, route-agnostic). Present ⇒ cards show 3 icon actions; absent ⇒ a single card that opens the table."),
|
|
14403
14404
|
SDATA(data_type_t.DTP_JSON, "landing_routes", 0, null, "Host-supplied hashes for the two landing sub-views {cards, schema}; the toggle navigates to them so the landing is URL-addressable (ROUTING.md). Absent ⇒ toggle flips in-view only (legacy)."),
|
|
14404
14405
|
SDATA(data_type_t.DTP_STRING, "base_route", 0, "", "This view's base route (host-supplied); used to declare its sub-routes (topics / info / schema) to the site map (ROUTING.md contributor)."),
|
|
@@ -15179,12 +15180,14 @@ function process_treedb_descs$1(gobj) {
|
|
|
15179
15180
|
let descs = gobj_read_attr(gobj, "descs");
|
|
15180
15181
|
let system = gobj_read_bool_attr(gobj, "system");
|
|
15181
15182
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
15183
|
+
let readonly = gobj_read_bool_attr(gobj, "readonly");
|
|
15182
15184
|
for (const [key, desc] of Object.entries(descs)) {
|
|
15183
15185
|
if (system) {} else if (key.substring(0, 2) === "__") continue;
|
|
15184
15186
|
let kw_topic_form = {
|
|
15185
15187
|
subscriber: gobj,
|
|
15186
|
-
|
|
15187
|
-
|
|
15188
|
+
readonly,
|
|
15189
|
+
with_new_button: !readonly,
|
|
15190
|
+
with_delete_button: !readonly,
|
|
15188
15191
|
treedb_name,
|
|
15189
15192
|
topic_name: key,
|
|
15190
15193
|
desc
|
|
@@ -15489,6 +15492,19 @@ function request_print_tranger$1(gobj, path) {
|
|
|
15489
15492
|
}, gobj);
|
|
15490
15493
|
if (ret) log_error(ret);
|
|
15491
15494
|
}
|
|
15495
|
+
/***************************************************************
|
|
15496
|
+
* The last gate before a write leaves for the yuno. The per-topic
|
|
15497
|
+
* children already hide their write buttons when the treedb is
|
|
15498
|
+
* read-only, but the record events converge HERE, and this is the
|
|
15499
|
+
* gclass that owns the treedb: a write that got this far on a replica
|
|
15500
|
+
* would travel, be refused by the yuno, and come back as a toast --
|
|
15501
|
+
* after the table had already drawn the row.
|
|
15502
|
+
***************************************************************/
|
|
15503
|
+
function refuse_if_readonly$1(gobj, event) {
|
|
15504
|
+
if (!gobj_read_bool_attr(gobj, "readonly")) return false;
|
|
15505
|
+
log_error(`${gobj_short_name(gobj)}: ${event} refused, treedb '${gobj_read_str_attr(gobj, "treedb_name")}' is READ-ONLY`);
|
|
15506
|
+
return true;
|
|
15507
|
+
}
|
|
15492
15508
|
/***************************
|
|
15493
15509
|
* Actions
|
|
15494
15510
|
***************************/
|
|
@@ -15761,6 +15777,7 @@ function ac_treedb_node_deleted$1(gobj, event, kw, src) {
|
|
|
15761
15777
|
* }
|
|
15762
15778
|
********************************************/
|
|
15763
15779
|
function ac_create_record(gobj, event, kw, src) {
|
|
15780
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
15764
15781
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
15765
15782
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
15766
15783
|
let record = kw.record;
|
|
@@ -15778,6 +15795,7 @@ function ac_create_record(gobj, event, kw, src) {
|
|
|
15778
15795
|
* }
|
|
15779
15796
|
********************************************/
|
|
15780
15797
|
function ac_update_record(gobj, event, kw, src) {
|
|
15798
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
15781
15799
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
15782
15800
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
15783
15801
|
let record = kw.record;
|
|
@@ -15794,6 +15812,7 @@ function ac_update_record(gobj, event, kw, src) {
|
|
|
15794
15812
|
* }
|
|
15795
15813
|
********************************************/
|
|
15796
15814
|
function ac_delete_record(gobj, event, kw, src) {
|
|
15815
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
15797
15816
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
15798
15817
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
15799
15818
|
let record = kw.record;
|
|
@@ -16081,6 +16100,68 @@ function yui_tabulator_relocalize(table, t) {
|
|
|
16081
16100
|
}
|
|
16082
16101
|
}
|
|
16083
16102
|
//#endregion
|
|
16103
|
+
//#region src/treedb_write_plan.js
|
|
16104
|
+
/***********************************************************************
|
|
16105
|
+
* treedb_write_plan.js
|
|
16106
|
+
*
|
|
16107
|
+
* Which write affordances a treedb topic offers.
|
|
16108
|
+
*
|
|
16109
|
+
* Pure, so it can be tested without a DOM: the gclass turns the
|
|
16110
|
+
* plan into buttons and columns, this decides the plan. Same split
|
|
16111
|
+
* as form_toolbar_plan.js.
|
|
16112
|
+
*
|
|
16113
|
+
* The rule it exists to state ONCE: `readonly` is not one more
|
|
16114
|
+
* button flag, it is the STATE of the topic and it wins over all of
|
|
16115
|
+
* them. A treedb whose tranger this yuno does not master answers
|
|
16116
|
+
* every write with "READ-ONLY" (the yuno refuses since SDK 7.13.0),
|
|
16117
|
+
* so offering the buttons anyway only turns a fact into an error
|
|
16118
|
+
* message per click. Written as `!readonly && with_x` in five
|
|
16119
|
+
* places it is five places to forget the sixth.
|
|
16120
|
+
*
|
|
16121
|
+
* Copyright (c) 2026, ArtGins.
|
|
16122
|
+
* All Rights Reserved.
|
|
16123
|
+
***********************************************************************/
|
|
16124
|
+
var READONLY_FORM_TOOLBAR = ["copy"];
|
|
16125
|
+
/***************************************************************
|
|
16126
|
+
* plan_treedb_writes(flags) -> plan
|
|
16127
|
+
*
|
|
16128
|
+
* flags {readonly, with_edition_mode, with_new_button,
|
|
16129
|
+
* with_delete_button, with_paste_button,
|
|
16130
|
+
* with_in_row_edit_icons}
|
|
16131
|
+
* Every with_* defaults to TRUE when absent, which is
|
|
16132
|
+
* what the gclass attrs default to: a caller that sets
|
|
16133
|
+
* nothing gets the toolbar this view always had.
|
|
16134
|
+
*
|
|
16135
|
+
* plan the same names without the `with_` prefix, plus
|
|
16136
|
+
* `form_toolbar`: the list to hand C_YUI_FORM, or null
|
|
16137
|
+
* to leave the form's own default alone.
|
|
16138
|
+
***************************************************************/
|
|
16139
|
+
function plan_treedb_writes(flags) {
|
|
16140
|
+
let f = flags || {};
|
|
16141
|
+
let readonly = !!f.readonly;
|
|
16142
|
+
let want = (name) => {
|
|
16143
|
+
return f[name] === void 0 ? true : !!f[name];
|
|
16144
|
+
};
|
|
16145
|
+
if (readonly) return {
|
|
16146
|
+
readonly: true,
|
|
16147
|
+
edition_mode: false,
|
|
16148
|
+
new_button: false,
|
|
16149
|
+
delete_button: false,
|
|
16150
|
+
paste_button: false,
|
|
16151
|
+
in_row_icons: false,
|
|
16152
|
+
form_toolbar: READONLY_FORM_TOOLBAR.slice()
|
|
16153
|
+
};
|
|
16154
|
+
return {
|
|
16155
|
+
readonly: false,
|
|
16156
|
+
edition_mode: want("with_edition_mode"),
|
|
16157
|
+
new_button: want("with_new_button"),
|
|
16158
|
+
delete_button: want("with_delete_button"),
|
|
16159
|
+
paste_button: want("with_paste_button"),
|
|
16160
|
+
in_row_icons: want("with_in_row_edit_icons"),
|
|
16161
|
+
form_toolbar: null
|
|
16162
|
+
};
|
|
16163
|
+
}
|
|
16164
|
+
//#endregion
|
|
16084
16165
|
//#region src/c_yui_treedb_topic_with_form.js
|
|
16085
16166
|
/***********************************************************************
|
|
16086
16167
|
* c_yui_treedb_topic_with_form.js
|
|
@@ -16102,6 +16183,7 @@ var attrs_table$4 = [
|
|
|
16102
16183
|
SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote service treedb name"),
|
|
16103
16184
|
SDATA(data_type_t.DTP_STRING, "topic_name", 0, null, "Topic name"),
|
|
16104
16185
|
SDATA(data_type_t.DTP_POINTER, "desc", 0, null, "Description of topics"),
|
|
16186
|
+
SDATA(data_type_t.DTP_BOOLEAN, "readonly", 0, false, "The topic cannot be written: no edition mode, no new/delete/paste, no in-row icons, and the record form opens without its save toolbar. Set it when the treedb is not the master of its tranger (only the master can write; the yuno refuses otherwise), or when the user lacks the authz"),
|
|
16105
16187
|
SDATA(data_type_t.DTP_BOOLEAN, "with_edition_mode", 0, true, "Enable EDITION mode showing EDIT Button toolbar"),
|
|
16106
16188
|
SDATA(data_type_t.DTP_BOOLEAN, "with_new_button", 0, true, "Button toolbar NEW"),
|
|
16107
16189
|
SDATA(data_type_t.DTP_BOOLEAN, "with_delete_button", 0, true, "Button toolbar DELETE"),
|
|
@@ -16264,11 +16346,12 @@ async function readClipboard(gobj) {
|
|
|
16264
16346
|
function build_ui$4(gobj) {
|
|
16265
16347
|
function create_table_toolbar() {
|
|
16266
16348
|
let $table_toolbar = [];
|
|
16267
|
-
let
|
|
16268
|
-
let
|
|
16269
|
-
let
|
|
16349
|
+
let plan = write_plan(gobj);
|
|
16350
|
+
let with_edition_mode = plan.edition_mode;
|
|
16351
|
+
let with_new_button = plan.new_button;
|
|
16352
|
+
let with_delete_button = plan.delete_button;
|
|
16270
16353
|
let with_copy_button = gobj_read_bool_attr(gobj, "with_copy_button");
|
|
16271
|
-
let with_paste_button =
|
|
16354
|
+
let with_paste_button = plan.paste_button;
|
|
16272
16355
|
let toolbar_id = gobj_read_str_attr(gobj, "toolbar_id");
|
|
16273
16356
|
if (with_edition_mode) {
|
|
16274
16357
|
$table_toolbar = createElement2(["div", {
|
|
@@ -16711,7 +16794,7 @@ function create_tabulator(gobj) {
|
|
|
16711
16794
|
"</button>"
|
|
16712
16795
|
].join("");
|
|
16713
16796
|
}
|
|
16714
|
-
if (
|
|
16797
|
+
if (write_plan(gobj).in_row_icons) columns.push({
|
|
16715
16798
|
field: "_operation",
|
|
16716
16799
|
title: "Op",
|
|
16717
16800
|
hozAlign: "center",
|
|
@@ -16990,15 +17073,17 @@ function open_form_dialog(gobj, mode, record) {
|
|
|
16990
17073
|
},
|
|
16991
17074
|
[]
|
|
16992
17075
|
]);
|
|
17076
|
+
let form_plan = write_plan(gobj);
|
|
16993
17077
|
let form = gobj_create_pure_child("form_" + clean_name(gobj_name(gobj)), "C_YUI_FORM", {
|
|
16994
17078
|
template: build_form_template(gobj),
|
|
16995
17079
|
record,
|
|
16996
17080
|
fkey_options: build_fkey_options(gobj),
|
|
16997
17081
|
form_mode: mode,
|
|
17082
|
+
...form_plan.form_toolbar ? { toolbar: form_plan.form_toolbar } : {},
|
|
16998
17083
|
render_mode: "edit",
|
|
16999
17084
|
pkey: desc.pkey || "id",
|
|
17000
17085
|
topic_name,
|
|
17001
|
-
editable:
|
|
17086
|
+
editable: !form_plan.readonly,
|
|
17002
17087
|
$parent: $body
|
|
17003
17088
|
}, gobj);
|
|
17004
17089
|
priv.form = form;
|
|
@@ -17525,6 +17610,34 @@ function show_dropdown_popup_menu(gobj, x, y, items, callback) {
|
|
|
17525
17610
|
let $with_focus = $element.querySelector(".with-focus");
|
|
17526
17611
|
if ($with_focus) $with_focus.focus();
|
|
17527
17612
|
}
|
|
17613
|
+
/***************************************************************
|
|
17614
|
+
* A read-only topic refuses the write EVENTS too, not only their
|
|
17615
|
+
* buttons. The buttons are gone, but an event can still arrive: a
|
|
17616
|
+
* keyboard path, a hosted form that outlived the flag, a caller that
|
|
17617
|
+
* sends EV_SAVE_RECORD itself. Refusing here is what makes `readonly`
|
|
17618
|
+
* a state of the gclass rather than a way of drawing it -- and it
|
|
17619
|
+
* fails LOUDLY, because an ignored write is how the backend used to
|
|
17620
|
+
* behave and what this whole change exists to stop.
|
|
17621
|
+
***************************************************************/
|
|
17622
|
+
function write_plan(gobj) {
|
|
17623
|
+
return plan_treedb_writes({
|
|
17624
|
+
readonly: gobj_read_bool_attr(gobj, "readonly"),
|
|
17625
|
+
with_edition_mode: gobj_read_bool_attr(gobj, "with_edition_mode"),
|
|
17626
|
+
with_new_button: gobj_read_bool_attr(gobj, "with_new_button"),
|
|
17627
|
+
with_delete_button: gobj_read_bool_attr(gobj, "with_delete_button"),
|
|
17628
|
+
with_paste_button: gobj_read_bool_attr(gobj, "with_paste_button"),
|
|
17629
|
+
with_in_row_edit_icons: gobj_read_bool_attr(gobj, "with_in_row_edit_icons")
|
|
17630
|
+
});
|
|
17631
|
+
}
|
|
17632
|
+
/***************************************************************
|
|
17633
|
+
* A read-only topic refuses the write EVENTS too, not only their
|
|
17634
|
+
* buttons.
|
|
17635
|
+
***************************************************************/
|
|
17636
|
+
function refuse_if_readonly(gobj, event) {
|
|
17637
|
+
if (!gobj_read_bool_attr(gobj, "readonly")) return false;
|
|
17638
|
+
log_error(`${gobj_short_name(gobj)}: ${event} refused, topic '${gobj_read_str_attr(gobj, "topic_name")}' is READ-ONLY`);
|
|
17639
|
+
return true;
|
|
17640
|
+
}
|
|
17528
17641
|
/***************************
|
|
17529
17642
|
* Actions
|
|
17530
17643
|
***************************/
|
|
@@ -17617,6 +17730,7 @@ function ac_node_deleted$1(gobj, event, kw, src) {
|
|
|
17617
17730
|
* and the Op column is visible (with row's edit/delete icons)
|
|
17618
17731
|
************************************************************/
|
|
17619
17732
|
function ac_edition_mode(gobj, event, kw, src) {
|
|
17733
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
17620
17734
|
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
17621
17735
|
let editable = gobj_read_bool_attr(gobj, "editable");
|
|
17622
17736
|
editable = !editable;
|
|
@@ -17660,6 +17774,7 @@ function ac_edition_mode(gobj, event, kw, src) {
|
|
|
17660
17774
|
* - the backend will broadcast the new record that will be added to table
|
|
17661
17775
|
************************************************************/
|
|
17662
17776
|
function ac_new_row(gobj, event, kw, src) {
|
|
17777
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
17663
17778
|
let row = {};
|
|
17664
17779
|
let desc = gobj_read_attr(gobj, "desc");
|
|
17665
17780
|
for (let i = 0; i < desc.cols.length; i++) {
|
|
@@ -17689,6 +17804,7 @@ function ac_new_row(gobj, event, kw, src) {
|
|
|
17689
17804
|
* kw {index:, row:}
|
|
17690
17805
|
************************************************************/
|
|
17691
17806
|
function ac_delete_rows(gobj, event, kw, src) {
|
|
17807
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
17692
17808
|
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
17693
17809
|
if (json_size(kw) === 0) {
|
|
17694
17810
|
let rows = tabulator.getSelectedData();
|
|
@@ -17752,6 +17868,7 @@ function ac_copy_rows(gobj, event, kw, src) {
|
|
|
17752
17868
|
* From internal
|
|
17753
17869
|
************************************************************/
|
|
17754
17870
|
function ac_paste_rows(gobj, event, kw, src) {
|
|
17871
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
17755
17872
|
if (kw.topic_name === gobj_read_str_attr(gobj, "topic_name") && kw.treedb_name === gobj_read_str_attr(gobj, "treedb_name")) for (let row of kw.rows) gobj_publish_event(gobj, "EV_CREATE_RECORD", {
|
|
17756
17873
|
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
17757
17874
|
record: row
|
|
@@ -17764,6 +17881,7 @@ function ac_paste_rows(gobj, event, kw, src) {
|
|
|
17764
17881
|
* fkeys, times and numbers). Route by the form's mode.
|
|
17765
17882
|
************************************************************/
|
|
17766
17883
|
function ac_form_save_record(gobj, event, kw, src) {
|
|
17884
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
17767
17885
|
let mode = gobj_read_str_attr(src, "form_mode");
|
|
17768
17886
|
gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", {
|
|
17769
17887
|
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
package/package.json
CHANGED
|
@@ -76,6 +76,8 @@ import {yui_tabulator_lang, yui_tabulator_relocalize} from "./yui_tabulator_i18n
|
|
|
76
76
|
|
|
77
77
|
import {t} from "i18next";
|
|
78
78
|
|
|
79
|
+
import {plan_treedb_writes} from "./treedb_write_plan.js";
|
|
80
|
+
|
|
79
81
|
import "./c_yui_treedb_topic_with_form.css";
|
|
80
82
|
import "./tabulator.css";
|
|
81
83
|
|
|
@@ -97,6 +99,7 @@ SDATA(data_type_t.DTP_STRING, "topic_name", 0, null, "Topic name"
|
|
|
97
99
|
SDATA(data_type_t.DTP_POINTER, "desc", 0, null, "Description of topics"),
|
|
98
100
|
|
|
99
101
|
/*---------------- Edition Mode ----------------*/
|
|
102
|
+
SDATA(data_type_t.DTP_BOOLEAN, "readonly", 0, false, "The topic cannot be written: no edition mode, no new/delete/paste, no in-row icons, and the record form opens without its save toolbar. Set it when the treedb is not the master of its tranger (only the master can write; the yuno refuses otherwise), or when the user lacks the authz"),
|
|
100
103
|
SDATA(data_type_t.DTP_BOOLEAN, "with_edition_mode", 0, true, "Enable EDITION mode showing EDIT Button toolbar"),
|
|
101
104
|
SDATA(data_type_t.DTP_BOOLEAN, "with_new_button", 0, true, "Button toolbar NEW"),
|
|
102
105
|
SDATA(data_type_t.DTP_BOOLEAN, "with_delete_button", 0, true, "Button toolbar DELETE"),
|
|
@@ -343,11 +346,16 @@ function build_ui(gobj)
|
|
|
343
346
|
// TODO pon autorización, solo si está autorizado a modificar los datos!!!
|
|
344
347
|
// TODO deja que estos botones se queden en el top cuando se hace scroll (clip ?)
|
|
345
348
|
let $table_toolbar = [];
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
+
/* One plan decides every write affordance (treedb_write_plan.js):
|
|
350
|
+
* `readonly` is the STATE of the topic and beats each with_* flag,
|
|
351
|
+
* and stating that in five `!readonly &&` expressions is five places
|
|
352
|
+
* to forget the sixth. */
|
|
353
|
+
let plan = write_plan(gobj);
|
|
354
|
+
let with_edition_mode = plan.edition_mode;
|
|
355
|
+
let with_new_button = plan.new_button;
|
|
356
|
+
let with_delete_button = plan.delete_button;
|
|
349
357
|
let with_copy_button = gobj_read_bool_attr(gobj, "with_copy_button");
|
|
350
|
-
let with_paste_button =
|
|
358
|
+
let with_paste_button = plan.paste_button;
|
|
351
359
|
|
|
352
360
|
let toolbar_id = gobj_read_str_attr(gobj, "toolbar_id");
|
|
353
361
|
|
|
@@ -884,7 +892,7 @@ function create_tabulator(gobj)
|
|
|
884
892
|
].join('');
|
|
885
893
|
}
|
|
886
894
|
|
|
887
|
-
let with_in_row_edit_icons =
|
|
895
|
+
let with_in_row_edit_icons = write_plan(gobj).in_row_icons;
|
|
888
896
|
if(with_in_row_edit_icons) {
|
|
889
897
|
columns.push({
|
|
890
898
|
field: '_operation',
|
|
@@ -1306,6 +1314,7 @@ function open_form_dialog(gobj, mode, record)
|
|
|
1306
1314
|
style: 'display:flex; flex-direction:column; height:100%;'}, []]
|
|
1307
1315
|
);
|
|
1308
1316
|
|
|
1317
|
+
let form_plan = write_plan(gobj);
|
|
1309
1318
|
let form = gobj_create_pure_child(
|
|
1310
1319
|
"form_" + clean_name(gobj_name(gobj)),
|
|
1311
1320
|
"C_YUI_FORM",
|
|
@@ -1314,13 +1323,18 @@ function open_form_dialog(gobj, mode, record)
|
|
|
1314
1323
|
record: record,
|
|
1315
1324
|
fkey_options: build_fkey_options(gobj),
|
|
1316
1325
|
form_mode: mode,
|
|
1326
|
+
/* A read-only topic still OPENS its form -- looking at a record
|
|
1327
|
+
* is the point of a replica -- with the cells not editable and
|
|
1328
|
+
* the write half of the toolbar gone. A null plan leaves the
|
|
1329
|
+
* form's own default alone. */
|
|
1330
|
+
...(form_plan.form_toolbar ? {toolbar: form_plan.form_toolbar} : {}),
|
|
1317
1331
|
/* Editing the raw topic record: structured cols
|
|
1318
1332
|
* (template / table / coordinates) are raw JSON editors,
|
|
1319
1333
|
* not interpreted into sub-widgets (the pre-merge behaviour). */
|
|
1320
1334
|
render_mode: "edit",
|
|
1321
1335
|
pkey: desc.pkey || "id",
|
|
1322
1336
|
topic_name: topic_name,
|
|
1323
|
-
editable:
|
|
1337
|
+
editable: !form_plan.readonly,
|
|
1324
1338
|
$parent: $body
|
|
1325
1339
|
},
|
|
1326
1340
|
gobj
|
|
@@ -2066,6 +2080,42 @@ function show_dropdown_popup_menu(gobj, x, y, items, callback)
|
|
|
2066
2080
|
|
|
2067
2081
|
|
|
2068
2082
|
|
|
2083
|
+
/***************************************************************
|
|
2084
|
+
* A read-only topic refuses the write EVENTS too, not only their
|
|
2085
|
+
* buttons. The buttons are gone, but an event can still arrive: a
|
|
2086
|
+
* keyboard path, a hosted form that outlived the flag, a caller that
|
|
2087
|
+
* sends EV_SAVE_RECORD itself. Refusing here is what makes `readonly`
|
|
2088
|
+
* a state of the gclass rather than a way of drawing it -- and it
|
|
2089
|
+
* fails LOUDLY, because an ignored write is how the backend used to
|
|
2090
|
+
* behave and what this whole change exists to stop.
|
|
2091
|
+
***************************************************************/
|
|
2092
|
+
function write_plan(gobj)
|
|
2093
|
+
{
|
|
2094
|
+
return plan_treedb_writes({
|
|
2095
|
+
readonly: gobj_read_bool_attr(gobj, "readonly"),
|
|
2096
|
+
with_edition_mode: gobj_read_bool_attr(gobj, "with_edition_mode"),
|
|
2097
|
+
with_new_button: gobj_read_bool_attr(gobj, "with_new_button"),
|
|
2098
|
+
with_delete_button: gobj_read_bool_attr(gobj, "with_delete_button"),
|
|
2099
|
+
with_paste_button: gobj_read_bool_attr(gobj, "with_paste_button"),
|
|
2100
|
+
with_in_row_edit_icons: gobj_read_bool_attr(gobj, "with_in_row_edit_icons")
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
/***************************************************************
|
|
2105
|
+
* A read-only topic refuses the write EVENTS too, not only their
|
|
2106
|
+
* buttons.
|
|
2107
|
+
***************************************************************/
|
|
2108
|
+
function refuse_if_readonly(gobj, event)
|
|
2109
|
+
{
|
|
2110
|
+
if(!gobj_read_bool_attr(gobj, "readonly")) {
|
|
2111
|
+
return false;
|
|
2112
|
+
}
|
|
2113
|
+
log_error(`${gobj_short_name(gobj)}: ${event} refused, topic ` +
|
|
2114
|
+
`'${gobj_read_str_attr(gobj, "topic_name")}' is READ-ONLY`);
|
|
2115
|
+
return true;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
|
|
2069
2119
|
/***************************
|
|
2070
2120
|
* Actions
|
|
2071
2121
|
***************************/
|
|
@@ -2242,6 +2292,9 @@ function ac_node_deleted(gobj, event, kw, src)
|
|
|
2242
2292
|
************************************************************/
|
|
2243
2293
|
function ac_edition_mode(gobj, event, kw, src)
|
|
2244
2294
|
{
|
|
2295
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2296
|
+
return -1; /* Error already logged */
|
|
2297
|
+
}
|
|
2245
2298
|
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
2246
2299
|
|
|
2247
2300
|
/*
|
|
@@ -2307,6 +2360,9 @@ function ac_edition_mode(gobj, event, kw, src)
|
|
|
2307
2360
|
************************************************************/
|
|
2308
2361
|
function ac_new_row(gobj, event, kw, src)
|
|
2309
2362
|
{
|
|
2363
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2364
|
+
return -1; /* Error already logged */
|
|
2365
|
+
}
|
|
2310
2366
|
/*
|
|
2311
2367
|
* Build default values. TODO no debería estar en desc configuration?
|
|
2312
2368
|
*/
|
|
@@ -2352,6 +2408,9 @@ function ac_new_row(gobj, event, kw, src)
|
|
|
2352
2408
|
************************************************************/
|
|
2353
2409
|
function ac_delete_rows(gobj, event, kw, src)
|
|
2354
2410
|
{
|
|
2411
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2412
|
+
return -1; /* Error already logged */
|
|
2413
|
+
}
|
|
2355
2414
|
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
2356
2415
|
|
|
2357
2416
|
if(json_size(kw) === 0) {
|
|
@@ -2451,6 +2510,9 @@ function ac_copy_rows(gobj, event, kw, src)
|
|
|
2451
2510
|
************************************************************/
|
|
2452
2511
|
function ac_paste_rows(gobj, event, kw, src)
|
|
2453
2512
|
{
|
|
2513
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2514
|
+
return -1; /* Error already logged */
|
|
2515
|
+
}
|
|
2454
2516
|
/*----------------------------*
|
|
2455
2517
|
* Paste rows
|
|
2456
2518
|
*----------------------------*/
|
|
@@ -2478,6 +2540,9 @@ function ac_paste_rows(gobj, event, kw, src)
|
|
|
2478
2540
|
************************************************************/
|
|
2479
2541
|
function ac_form_save_record(gobj, event, kw, src)
|
|
2480
2542
|
{
|
|
2543
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2544
|
+
return -1; /* Error already logged */
|
|
2545
|
+
}
|
|
2481
2546
|
let mode = gobj_read_str_attr(src, "form_mode");
|
|
2482
2547
|
gobj_publish_event(
|
|
2483
2548
|
gobj,
|
|
@@ -77,6 +77,7 @@ SDATA(data_type_t.DTP_JSON, "descs", 0, null, "Description of
|
|
|
77
77
|
SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
|
|
78
78
|
SDATA(data_type_t.DTP_STRING, "tabs_style", 0, "is-toggle is-fullwidth", "Bulma tab styling"),
|
|
79
79
|
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)."),
|
|
80
|
+
SDATA(data_type_t.DTP_BOOLEAN, "readonly", 0, false, "The whole treedb is read-only: every topic opens without its write affordances and the write events are refused. Set it when this yuno is not the MASTER of the treedb's tranger -- only the master can write, and the yuno answers 'READ-ONLY' to a write since SDK 7.13.0 (ask `treedb-info`)"),
|
|
80
81
|
SDATA(data_type_t.DTP_JSON, "card_action_routes",0, null, "Per-card hash-route templates {info, table, graph} with a {topic} placeholder (host-supplied, route-agnostic). Present ⇒ cards show 3 icon actions; absent ⇒ a single card that opens the table."),
|
|
81
82
|
SDATA(data_type_t.DTP_JSON, "landing_routes", 0, null, "Host-supplied hashes for the two landing sub-views {cards, schema}; the toggle navigates to them so the landing is URL-addressable (ROUTING.md). Absent ⇒ toggle flips in-view only (legacy)."),
|
|
82
83
|
SDATA(data_type_t.DTP_STRING, "base_route", 0, "", "This view's base route (host-supplied); used to declare its sub-routes (topics / info / schema) to the site map (ROUTING.md contributor)."),
|
|
@@ -997,6 +998,7 @@ function process_treedb_descs(gobj)
|
|
|
997
998
|
let descs = gobj_read_attr(gobj, "descs");
|
|
998
999
|
let system = gobj_read_bool_attr(gobj, "system");
|
|
999
1000
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
1001
|
+
let readonly = gobj_read_bool_attr(gobj, "readonly");
|
|
1000
1002
|
for(const [key, desc] of Object.entries(descs)) {
|
|
1001
1003
|
if(system) {
|
|
1002
1004
|
} else {
|
|
@@ -1008,10 +1010,13 @@ function process_treedb_descs(gobj)
|
|
|
1008
1010
|
let kw_topic_form = {
|
|
1009
1011
|
subscriber: gobj, // HACK get all output events
|
|
1010
1012
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1013
|
+
/* One flag, not three: a read-only treedb has no writable
|
|
1014
|
+
* topic. (The old TODO here asked for the authz to decide the
|
|
1015
|
+
* buttons; the authz is a per-user question the backend answers
|
|
1016
|
+
* per command, while this is a property of the STORE.) */
|
|
1017
|
+
readonly: readonly,
|
|
1018
|
+
with_new_button: !readonly,
|
|
1019
|
+
with_delete_button: !readonly,
|
|
1015
1020
|
|
|
1016
1021
|
treedb_name: treedb_name,
|
|
1017
1022
|
topic_name: key,
|
|
@@ -1619,6 +1624,25 @@ function request_print_tranger(gobj, path)
|
|
|
1619
1624
|
|
|
1620
1625
|
|
|
1621
1626
|
|
|
1627
|
+
/***************************************************************
|
|
1628
|
+
* The last gate before a write leaves for the yuno. The per-topic
|
|
1629
|
+
* children already hide their write buttons when the treedb is
|
|
1630
|
+
* read-only, but the record events converge HERE, and this is the
|
|
1631
|
+
* gclass that owns the treedb: a write that got this far on a replica
|
|
1632
|
+
* would travel, be refused by the yuno, and come back as a toast --
|
|
1633
|
+
* after the table had already drawn the row.
|
|
1634
|
+
***************************************************************/
|
|
1635
|
+
function refuse_if_readonly(gobj, event)
|
|
1636
|
+
{
|
|
1637
|
+
if(!gobj_read_bool_attr(gobj, "readonly")) {
|
|
1638
|
+
return false;
|
|
1639
|
+
}
|
|
1640
|
+
log_error(`${gobj_short_name(gobj)}: ${event} refused, treedb ` +
|
|
1641
|
+
`'${gobj_read_str_attr(gobj, "treedb_name")}' is READ-ONLY`);
|
|
1642
|
+
return true;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
|
|
1622
1646
|
/***************************
|
|
1623
1647
|
* Actions
|
|
1624
1648
|
***************************/
|
|
@@ -2051,6 +2075,9 @@ function ac_treedb_node_deleted(gobj, event, kw, src)
|
|
|
2051
2075
|
********************************************/
|
|
2052
2076
|
function ac_create_record(gobj, event, kw, src)
|
|
2053
2077
|
{
|
|
2078
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2079
|
+
return -1; /* Error already logged */
|
|
2080
|
+
}
|
|
2054
2081
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
2055
2082
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
2056
2083
|
let record = kw.record;
|
|
@@ -2079,6 +2106,9 @@ function ac_create_record(gobj, event, kw, src)
|
|
|
2079
2106
|
********************************************/
|
|
2080
2107
|
function ac_update_record(gobj, event, kw, src)
|
|
2081
2108
|
{
|
|
2109
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2110
|
+
return -1; /* Error already logged */
|
|
2111
|
+
}
|
|
2082
2112
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
2083
2113
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
2084
2114
|
let record = kw.record;
|
|
@@ -2106,6 +2136,9 @@ function ac_update_record(gobj, event, kw, src)
|
|
|
2106
2136
|
********************************************/
|
|
2107
2137
|
function ac_delete_record(gobj, event, kw, src)
|
|
2108
2138
|
{
|
|
2139
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
2140
|
+
return -1; /* Error already logged */
|
|
2141
|
+
}
|
|
2109
2142
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
2110
2143
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
2111
2144
|
let record = kw.record;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_write_plan.js
|
|
3
|
+
*
|
|
4
|
+
* Which write affordances a treedb topic offers.
|
|
5
|
+
*
|
|
6
|
+
* Pure, so it can be tested without a DOM: the gclass turns the
|
|
7
|
+
* plan into buttons and columns, this decides the plan. Same split
|
|
8
|
+
* as form_toolbar_plan.js.
|
|
9
|
+
*
|
|
10
|
+
* The rule it exists to state ONCE: `readonly` is not one more
|
|
11
|
+
* button flag, it is the STATE of the topic and it wins over all of
|
|
12
|
+
* them. A treedb whose tranger this yuno does not master answers
|
|
13
|
+
* every write with "READ-ONLY" (the yuno refuses since SDK 7.13.0),
|
|
14
|
+
* so offering the buttons anyway only turns a fact into an error
|
|
15
|
+
* message per click. Written as `!readonly && with_x` in five
|
|
16
|
+
* places it is five places to forget the sixth.
|
|
17
|
+
*
|
|
18
|
+
* Copyright (c) 2026, ArtGins.
|
|
19
|
+
* All Rights Reserved.
|
|
20
|
+
***********************************************************************/
|
|
21
|
+
|
|
22
|
+
/* What a read-only form keeps: reading a record includes taking it with
|
|
23
|
+
* you, so `copy` stays and the write half goes. */
|
|
24
|
+
const READONLY_FORM_TOOLBAR = ["copy"];
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/***************************************************************
|
|
28
|
+
* plan_treedb_writes(flags) -> plan
|
|
29
|
+
*
|
|
30
|
+
* flags {readonly, with_edition_mode, with_new_button,
|
|
31
|
+
* with_delete_button, with_paste_button,
|
|
32
|
+
* with_in_row_edit_icons}
|
|
33
|
+
* Every with_* defaults to TRUE when absent, which is
|
|
34
|
+
* what the gclass attrs default to: a caller that sets
|
|
35
|
+
* nothing gets the toolbar this view always had.
|
|
36
|
+
*
|
|
37
|
+
* plan the same names without the `with_` prefix, plus
|
|
38
|
+
* `form_toolbar`: the list to hand C_YUI_FORM, or null
|
|
39
|
+
* to leave the form's own default alone.
|
|
40
|
+
***************************************************************/
|
|
41
|
+
function plan_treedb_writes(flags)
|
|
42
|
+
{
|
|
43
|
+
let f = flags || {};
|
|
44
|
+
let readonly = !!f.readonly;
|
|
45
|
+
let want = (name) => {
|
|
46
|
+
return (f[name] === undefined) ? true : !!f[name];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if(readonly) {
|
|
50
|
+
return {
|
|
51
|
+
readonly: true,
|
|
52
|
+
edition_mode: false,
|
|
53
|
+
new_button: false,
|
|
54
|
+
delete_button: false,
|
|
55
|
+
paste_button: false,
|
|
56
|
+
in_row_icons: false,
|
|
57
|
+
form_toolbar: READONLY_FORM_TOOLBAR.slice()
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
readonly: false,
|
|
63
|
+
edition_mode: want("with_edition_mode"),
|
|
64
|
+
new_button: want("with_new_button"),
|
|
65
|
+
delete_button: want("with_delete_button"),
|
|
66
|
+
paste_button: want("with_paste_button"),
|
|
67
|
+
in_row_icons: want("with_in_row_edit_icons"),
|
|
68
|
+
form_toolbar: null
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
export {plan_treedb_writes, READONLY_FORM_TOOLBAR};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_write_plan.test.js
|
|
3
|
+
*
|
|
4
|
+
* What a read-only treedb topic must NOT offer, pinned.
|
|
5
|
+
*
|
|
6
|
+
* The point of the file under test is that one flag beats five, so
|
|
7
|
+
* these tests are mostly about the ways someone could get a write
|
|
8
|
+
* affordance back by accident.
|
|
9
|
+
***********************************************************************/
|
|
10
|
+
import { describe, test, expect } from "vitest";
|
|
11
|
+
import { plan_treedb_writes, READONLY_FORM_TOOLBAR } from "./treedb_write_plan.js";
|
|
12
|
+
|
|
13
|
+
const WRITES = ["edition_mode", "new_button", "delete_button", "paste_button", "in_row_icons"];
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
describe("writable (the default)", () => {
|
|
17
|
+
test("nothing set is everything on — the toolbar this view always had", () => {
|
|
18
|
+
const plan = plan_treedb_writes({});
|
|
19
|
+
for(const k of WRITES) {
|
|
20
|
+
expect(plan[k]).toBe(true);
|
|
21
|
+
}
|
|
22
|
+
expect(plan.readonly).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("an absent flags object behaves the same", () => {
|
|
26
|
+
expect(plan_treedb_writes(undefined).new_button).toBe(true);
|
|
27
|
+
expect(plan_treedb_writes(null).delete_button).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("the form keeps its OWN default toolbar (null, not a copy of it)", () => {
|
|
31
|
+
/* Repeating the five names here is how the default drifts from
|
|
32
|
+
* C_YUI_FORM's; null says "do not touch it". */
|
|
33
|
+
expect(plan_treedb_writes({}).form_toolbar).toBe(null);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a single with_* off turns off only that one", () => {
|
|
37
|
+
const plan = plan_treedb_writes({with_delete_button: false});
|
|
38
|
+
expect(plan.delete_button).toBe(false);
|
|
39
|
+
expect(plan.new_button).toBe(true);
|
|
40
|
+
expect(plan.edition_mode).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
describe("readonly beats every with_*", () => {
|
|
46
|
+
test("all write affordances off, even when each is explicitly asked for", () => {
|
|
47
|
+
const plan = plan_treedb_writes({
|
|
48
|
+
readonly: true,
|
|
49
|
+
with_edition_mode: true,
|
|
50
|
+
with_new_button: true,
|
|
51
|
+
with_delete_button: true,
|
|
52
|
+
with_paste_button: true,
|
|
53
|
+
with_in_row_edit_icons: true
|
|
54
|
+
});
|
|
55
|
+
for(const k of WRITES) {
|
|
56
|
+
expect(plan[k]).toBe(false);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("the form opens with copy only — reading includes taking the record", () => {
|
|
61
|
+
expect(plan_treedb_writes({readonly: true}).form_toolbar).toEqual(["copy"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("the plan hands out a COPY of the toolbar, so a caller cannot mutate the constant", () => {
|
|
65
|
+
const plan = plan_treedb_writes({readonly: true});
|
|
66
|
+
plan.form_toolbar.push("save");
|
|
67
|
+
expect(READONLY_FORM_TOOLBAR).toEqual(["copy"]);
|
|
68
|
+
expect(plan_treedb_writes({readonly: true}).form_toolbar).toEqual(["copy"]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("readonly is reported, so the gclass can refuse the EVENTS too", () => {
|
|
72
|
+
/* Hiding the buttons is not the same as refusing the write: an
|
|
73
|
+
* event can still arrive from a keyboard path or a stale form. */
|
|
74
|
+
expect(plan_treedb_writes({readonly: true}).readonly).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("only a truthy readonly counts — a missing flag is writable", () => {
|
|
78
|
+
expect(plan_treedb_writes({readonly: false}).new_button).toBe(true);
|
|
79
|
+
expect(plan_treedb_writes({readonly: 0}).new_button).toBe(true);
|
|
80
|
+
expect(plan_treedb_writes({readonly: undefined}).new_button).toBe(true);
|
|
81
|
+
expect(plan_treedb_writes({readonly: "yes"}).new_button).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|