@yuneta/gobj-ui 5.14.2 → 5.16.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 +39 -0
- package/dist/gobj-ui.cjs.js +179 -17
- package/dist/gobj-ui.es.js +179 -17
- package/package.json +1 -1
- package/src/c_yui_treedb_graph.js +99 -1
- package/src/c_yui_treedb_topic_with_form.js +87 -7
- package/src/c_yui_treedb_topics.js +39 -5
- package/src/treedb_write_plan.js +73 -0
- package/src/treedb_write_plan.test.js +83 -0
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$2(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
|
***************************/
|
|
@@ -15556,7 +15572,8 @@ function ac_mt_command_answer$1(gobj, event, kw, src) {
|
|
|
15556
15572
|
treedb_name: kw_get_str(gobj, kw_command, "treedb_name", "", 0),
|
|
15557
15573
|
topic_name: kw_get_str(gobj, kw_command, "topic_name", "", 0),
|
|
15558
15574
|
record: kw_get_dict(gobj, kw_command, "record", {}, 0),
|
|
15559
|
-
created: command === "create-node"
|
|
15575
|
+
created: command === "create-node",
|
|
15576
|
+
command
|
|
15560
15577
|
});
|
|
15561
15578
|
break;
|
|
15562
15579
|
case "delete-node": break;
|
|
@@ -15761,6 +15778,7 @@ function ac_treedb_node_deleted$1(gobj, event, kw, src) {
|
|
|
15761
15778
|
* }
|
|
15762
15779
|
********************************************/
|
|
15763
15780
|
function ac_create_record(gobj, event, kw, src) {
|
|
15781
|
+
if (refuse_if_readonly$2(gobj, event)) return -1;
|
|
15764
15782
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
15765
15783
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
15766
15784
|
let record = kw.record;
|
|
@@ -15778,6 +15796,7 @@ function ac_create_record(gobj, event, kw, src) {
|
|
|
15778
15796
|
* }
|
|
15779
15797
|
********************************************/
|
|
15780
15798
|
function ac_update_record(gobj, event, kw, src) {
|
|
15799
|
+
if (refuse_if_readonly$2(gobj, event)) return -1;
|
|
15781
15800
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
15782
15801
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
15783
15802
|
let record = kw.record;
|
|
@@ -15794,6 +15813,7 @@ function ac_update_record(gobj, event, kw, src) {
|
|
|
15794
15813
|
* }
|
|
15795
15814
|
********************************************/
|
|
15796
15815
|
function ac_delete_record(gobj, event, kw, src) {
|
|
15816
|
+
if (refuse_if_readonly$2(gobj, event)) return -1;
|
|
15797
15817
|
let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
|
|
15798
15818
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
15799
15819
|
let record = kw.record;
|
|
@@ -16081,6 +16101,68 @@ function yui_tabulator_relocalize(table, t) {
|
|
|
16081
16101
|
}
|
|
16082
16102
|
}
|
|
16083
16103
|
//#endregion
|
|
16104
|
+
//#region src/treedb_write_plan.js
|
|
16105
|
+
/***********************************************************************
|
|
16106
|
+
* treedb_write_plan.js
|
|
16107
|
+
*
|
|
16108
|
+
* Which write affordances a treedb topic offers.
|
|
16109
|
+
*
|
|
16110
|
+
* Pure, so it can be tested without a DOM: the gclass turns the
|
|
16111
|
+
* plan into buttons and columns, this decides the plan. Same split
|
|
16112
|
+
* as form_toolbar_plan.js.
|
|
16113
|
+
*
|
|
16114
|
+
* The rule it exists to state ONCE: `readonly` is not one more
|
|
16115
|
+
* button flag, it is the STATE of the topic and it wins over all of
|
|
16116
|
+
* them. A treedb whose tranger this yuno does not master answers
|
|
16117
|
+
* every write with "READ-ONLY" (the yuno refuses since SDK 7.13.0),
|
|
16118
|
+
* so offering the buttons anyway only turns a fact into an error
|
|
16119
|
+
* message per click. Written as `!readonly && with_x` in five
|
|
16120
|
+
* places it is five places to forget the sixth.
|
|
16121
|
+
*
|
|
16122
|
+
* Copyright (c) 2026, ArtGins.
|
|
16123
|
+
* All Rights Reserved.
|
|
16124
|
+
***********************************************************************/
|
|
16125
|
+
var READONLY_FORM_TOOLBAR = ["copy"];
|
|
16126
|
+
/***************************************************************
|
|
16127
|
+
* plan_treedb_writes(flags) -> plan
|
|
16128
|
+
*
|
|
16129
|
+
* flags {readonly, with_edition_mode, with_new_button,
|
|
16130
|
+
* with_delete_button, with_paste_button,
|
|
16131
|
+
* with_in_row_edit_icons}
|
|
16132
|
+
* Every with_* defaults to TRUE when absent, which is
|
|
16133
|
+
* what the gclass attrs default to: a caller that sets
|
|
16134
|
+
* nothing gets the toolbar this view always had.
|
|
16135
|
+
*
|
|
16136
|
+
* plan the same names without the `with_` prefix, plus
|
|
16137
|
+
* `form_toolbar`: the list to hand C_YUI_FORM, or null
|
|
16138
|
+
* to leave the form's own default alone.
|
|
16139
|
+
***************************************************************/
|
|
16140
|
+
function plan_treedb_writes(flags) {
|
|
16141
|
+
let f = flags || {};
|
|
16142
|
+
let readonly = !!f.readonly;
|
|
16143
|
+
let want = (name) => {
|
|
16144
|
+
return f[name] === void 0 ? true : !!f[name];
|
|
16145
|
+
};
|
|
16146
|
+
if (readonly) return {
|
|
16147
|
+
readonly: true,
|
|
16148
|
+
edition_mode: false,
|
|
16149
|
+
new_button: false,
|
|
16150
|
+
delete_button: false,
|
|
16151
|
+
paste_button: false,
|
|
16152
|
+
in_row_icons: false,
|
|
16153
|
+
form_toolbar: READONLY_FORM_TOOLBAR.slice()
|
|
16154
|
+
};
|
|
16155
|
+
return {
|
|
16156
|
+
readonly: false,
|
|
16157
|
+
edition_mode: want("with_edition_mode"),
|
|
16158
|
+
new_button: want("with_new_button"),
|
|
16159
|
+
delete_button: want("with_delete_button"),
|
|
16160
|
+
paste_button: want("with_paste_button"),
|
|
16161
|
+
in_row_icons: want("with_in_row_edit_icons"),
|
|
16162
|
+
form_toolbar: null
|
|
16163
|
+
};
|
|
16164
|
+
}
|
|
16165
|
+
//#endregion
|
|
16084
16166
|
//#region src/c_yui_treedb_topic_with_form.js
|
|
16085
16167
|
/***********************************************************************
|
|
16086
16168
|
* c_yui_treedb_topic_with_form.js
|
|
@@ -16102,6 +16184,7 @@ var attrs_table$4 = [
|
|
|
16102
16184
|
SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote service treedb name"),
|
|
16103
16185
|
SDATA(data_type_t.DTP_STRING, "topic_name", 0, null, "Topic name"),
|
|
16104
16186
|
SDATA(data_type_t.DTP_POINTER, "desc", 0, null, "Description of topics"),
|
|
16187
|
+
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
16188
|
SDATA(data_type_t.DTP_BOOLEAN, "with_edition_mode", 0, true, "Enable EDITION mode showing EDIT Button toolbar"),
|
|
16106
16189
|
SDATA(data_type_t.DTP_BOOLEAN, "with_new_button", 0, true, "Button toolbar NEW"),
|
|
16107
16190
|
SDATA(data_type_t.DTP_BOOLEAN, "with_delete_button", 0, true, "Button toolbar DELETE"),
|
|
@@ -16264,11 +16347,12 @@ async function readClipboard(gobj) {
|
|
|
16264
16347
|
function build_ui$4(gobj) {
|
|
16265
16348
|
function create_table_toolbar() {
|
|
16266
16349
|
let $table_toolbar = [];
|
|
16267
|
-
let
|
|
16268
|
-
let
|
|
16269
|
-
let
|
|
16350
|
+
let plan = write_plan(gobj);
|
|
16351
|
+
let with_edition_mode = plan.edition_mode;
|
|
16352
|
+
let with_new_button = plan.new_button;
|
|
16353
|
+
let with_delete_button = plan.delete_button;
|
|
16270
16354
|
let with_copy_button = gobj_read_bool_attr(gobj, "with_copy_button");
|
|
16271
|
-
let with_paste_button =
|
|
16355
|
+
let with_paste_button = plan.paste_button;
|
|
16272
16356
|
let toolbar_id = gobj_read_str_attr(gobj, "toolbar_id");
|
|
16273
16357
|
if (with_edition_mode) {
|
|
16274
16358
|
$table_toolbar = createElement2(["div", {
|
|
@@ -16711,7 +16795,7 @@ function create_tabulator(gobj) {
|
|
|
16711
16795
|
"</button>"
|
|
16712
16796
|
].join("");
|
|
16713
16797
|
}
|
|
16714
|
-
if (
|
|
16798
|
+
if (write_plan(gobj).in_row_icons) columns.push({
|
|
16715
16799
|
field: "_operation",
|
|
16716
16800
|
title: "Op",
|
|
16717
16801
|
hozAlign: "center",
|
|
@@ -16990,15 +17074,17 @@ function open_form_dialog(gobj, mode, record) {
|
|
|
16990
17074
|
},
|
|
16991
17075
|
[]
|
|
16992
17076
|
]);
|
|
17077
|
+
let form_plan = write_plan(gobj);
|
|
16993
17078
|
let form = gobj_create_pure_child("form_" + clean_name(gobj_name(gobj)), "C_YUI_FORM", {
|
|
16994
17079
|
template: build_form_template(gobj),
|
|
16995
17080
|
record,
|
|
16996
17081
|
fkey_options: build_fkey_options(gobj),
|
|
16997
17082
|
form_mode: mode,
|
|
17083
|
+
...form_plan.form_toolbar ? { toolbar: form_plan.form_toolbar } : {},
|
|
16998
17084
|
render_mode: "edit",
|
|
16999
17085
|
pkey: desc.pkey || "id",
|
|
17000
17086
|
topic_name,
|
|
17001
|
-
editable:
|
|
17087
|
+
editable: !form_plan.readonly,
|
|
17002
17088
|
$parent: $body
|
|
17003
17089
|
}, gobj);
|
|
17004
17090
|
priv.form = form;
|
|
@@ -17525,6 +17611,34 @@ function show_dropdown_popup_menu(gobj, x, y, items, callback) {
|
|
|
17525
17611
|
let $with_focus = $element.querySelector(".with-focus");
|
|
17526
17612
|
if ($with_focus) $with_focus.focus();
|
|
17527
17613
|
}
|
|
17614
|
+
/***************************************************************
|
|
17615
|
+
* A read-only topic refuses the write EVENTS too, not only their
|
|
17616
|
+
* buttons. The buttons are gone, but an event can still arrive: a
|
|
17617
|
+
* keyboard path, a hosted form that outlived the flag, a caller that
|
|
17618
|
+
* sends EV_SAVE_RECORD itself. Refusing here is what makes `readonly`
|
|
17619
|
+
* a state of the gclass rather than a way of drawing it -- and it
|
|
17620
|
+
* fails LOUDLY, because an ignored write is how the backend used to
|
|
17621
|
+
* behave and what this whole change exists to stop.
|
|
17622
|
+
***************************************************************/
|
|
17623
|
+
function write_plan(gobj) {
|
|
17624
|
+
return plan_treedb_writes({
|
|
17625
|
+
readonly: gobj_read_bool_attr(gobj, "readonly"),
|
|
17626
|
+
with_edition_mode: gobj_read_bool_attr(gobj, "with_edition_mode"),
|
|
17627
|
+
with_new_button: gobj_read_bool_attr(gobj, "with_new_button"),
|
|
17628
|
+
with_delete_button: gobj_read_bool_attr(gobj, "with_delete_button"),
|
|
17629
|
+
with_paste_button: gobj_read_bool_attr(gobj, "with_paste_button"),
|
|
17630
|
+
with_in_row_edit_icons: gobj_read_bool_attr(gobj, "with_in_row_edit_icons")
|
|
17631
|
+
});
|
|
17632
|
+
}
|
|
17633
|
+
/***************************************************************
|
|
17634
|
+
* A read-only topic refuses the write EVENTS too, not only their
|
|
17635
|
+
* buttons.
|
|
17636
|
+
***************************************************************/
|
|
17637
|
+
function refuse_if_readonly$1(gobj, event) {
|
|
17638
|
+
if (!gobj_read_bool_attr(gobj, "readonly")) return false;
|
|
17639
|
+
log_error(`${gobj_short_name(gobj)}: ${event} refused, topic '${gobj_read_str_attr(gobj, "topic_name")}' is READ-ONLY`);
|
|
17640
|
+
return true;
|
|
17641
|
+
}
|
|
17528
17642
|
/***************************
|
|
17529
17643
|
* Actions
|
|
17530
17644
|
***************************/
|
|
@@ -17592,7 +17706,10 @@ function ac_load_node_updated(gobj, event, kw, src) {
|
|
|
17592
17706
|
return -1;
|
|
17593
17707
|
}
|
|
17594
17708
|
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
17595
|
-
if (tabulator)
|
|
17709
|
+
if (tabulator) {
|
|
17710
|
+
for (let record of data) if (tabulator.getRow(record.id)) tabulator.updateData([record]);
|
|
17711
|
+
else if (tabulator.getDataCount() > 0) log_error(`${gobj_short_name(gobj)}: updated node is not in the loaded table: ${record.id}`);
|
|
17712
|
+
}
|
|
17596
17713
|
return 0;
|
|
17597
17714
|
}
|
|
17598
17715
|
/************************************************************
|
|
@@ -17617,6 +17734,7 @@ function ac_node_deleted$1(gobj, event, kw, src) {
|
|
|
17617
17734
|
* and the Op column is visible (with row's edit/delete icons)
|
|
17618
17735
|
************************************************************/
|
|
17619
17736
|
function ac_edition_mode(gobj, event, kw, src) {
|
|
17737
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
17620
17738
|
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
17621
17739
|
let editable = gobj_read_bool_attr(gobj, "editable");
|
|
17622
17740
|
editable = !editable;
|
|
@@ -17660,6 +17778,7 @@ function ac_edition_mode(gobj, event, kw, src) {
|
|
|
17660
17778
|
* - the backend will broadcast the new record that will be added to table
|
|
17661
17779
|
************************************************************/
|
|
17662
17780
|
function ac_new_row(gobj, event, kw, src) {
|
|
17781
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
17663
17782
|
let row = {};
|
|
17664
17783
|
let desc = gobj_read_attr(gobj, "desc");
|
|
17665
17784
|
for (let i = 0; i < desc.cols.length; i++) {
|
|
@@ -17689,6 +17808,7 @@ function ac_new_row(gobj, event, kw, src) {
|
|
|
17689
17808
|
* kw {index:, row:}
|
|
17690
17809
|
************************************************************/
|
|
17691
17810
|
function ac_delete_rows(gobj, event, kw, src) {
|
|
17811
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
17692
17812
|
let tabulator = gobj_read_attr(gobj, "tabulator");
|
|
17693
17813
|
if (json_size(kw) === 0) {
|
|
17694
17814
|
let rows = tabulator.getSelectedData();
|
|
@@ -17752,6 +17872,7 @@ function ac_copy_rows(gobj, event, kw, src) {
|
|
|
17752
17872
|
* From internal
|
|
17753
17873
|
************************************************************/
|
|
17754
17874
|
function ac_paste_rows(gobj, event, kw, src) {
|
|
17875
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
17755
17876
|
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
17877
|
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
17757
17878
|
record: row
|
|
@@ -17764,6 +17885,7 @@ function ac_paste_rows(gobj, event, kw, src) {
|
|
|
17764
17885
|
* fkeys, times and numbers). Route by the form's mode.
|
|
17765
17886
|
************************************************************/
|
|
17766
17887
|
function ac_form_save_record(gobj, event, kw, src) {
|
|
17888
|
+
if (refuse_if_readonly$1(gobj, event)) return -1;
|
|
17767
17889
|
let mode = gobj_read_str_attr(src, "form_mode");
|
|
17768
17890
|
gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", {
|
|
17769
17891
|
topic_name: gobj_read_str_attr(gobj, "topic_name"),
|
|
@@ -18171,6 +18293,7 @@ var attrs_table$3 = [
|
|
|
18171
18293
|
SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote service treedb name"),
|
|
18172
18294
|
SDATA(data_type_t.DTP_DICT, "descs", 0, null, "Descriptions of topics obtained"),
|
|
18173
18295
|
SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
|
|
18296
|
+
SDATA(data_type_t.DTP_BOOLEAN, "readonly", 0, false, "The whole treedb is read-only: the graph drops its `edition` mode (the only one that draws write affordances) and refuses every write event. 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`)"),
|
|
18174
18297
|
SDATA(data_type_t.DTP_STRING, "back_route", 0, "", "Optional hash route for a '← topics' button back to the topics grid (host-supplied; empty = no button)"),
|
|
18175
18298
|
SDATA(data_type_t.DTP_STRING, "base_route", 0, "", "This view's base route (host-supplied); used to declare its per-topic focus sub-routes to the site map (ROUTING.md contributor)."),
|
|
18176
18299
|
SDATA(data_type_t.DTP_DICT, "records", 0, "{}", "Data of topics"),
|
|
@@ -18221,6 +18344,7 @@ function mt_create$3(gobj) {
|
|
|
18221
18344
|
if (!subscriber) subscriber = gobj_parent(gobj);
|
|
18222
18345
|
gobj_subscribe_event(gobj, null, {}, subscriber);
|
|
18223
18346
|
if (!priv.treedb_name) log_error(`${gobj_name(gobj)} -> treedb_name not configured`);
|
|
18347
|
+
if (gobj_read_bool_attr(gobj, "readonly") && priv.operation_mode === "edition") gobj_write_str_attr(gobj, "operation_mode", "reading");
|
|
18224
18348
|
let canvas_id = clean_name(gobj_name(gobj)) + "-canvas";
|
|
18225
18349
|
gobj_write_attr(gobj, "canvas_id", canvas_id);
|
|
18226
18350
|
let $container_canvas = build_ui$3(gobj).querySelector(`#${priv.canvas_id}`);
|
|
@@ -18375,6 +18499,8 @@ function destroy_ui(gobj) {
|
|
|
18375
18499
|
************************************************************/
|
|
18376
18500
|
function make_toolbar(gobj) {
|
|
18377
18501
|
gobj.priv;
|
|
18502
|
+
let modes = gobj_read_attr(gobj, "operation_modes");
|
|
18503
|
+
if (gobj_read_bool_attr(gobj, "readonly")) modes = modes.filter((mode) => mode !== "edition");
|
|
18378
18504
|
const $toolbar_header = yui_toolbar({}, [
|
|
18379
18505
|
[
|
|
18380
18506
|
"div",
|
|
@@ -18413,7 +18539,7 @@ function make_toolbar(gobj) {
|
|
|
18413
18539
|
[[
|
|
18414
18540
|
"select",
|
|
18415
18541
|
{ class: "GRAPH_MODE_SELECT" },
|
|
18416
|
-
|
|
18542
|
+
modes.map((item) => [
|
|
18417
18543
|
"option",
|
|
18418
18544
|
{},
|
|
18419
18545
|
item
|
|
@@ -18965,6 +19091,19 @@ function refresh_data(gobj) {
|
|
|
18965
19091
|
if (priv.gobj_nodes_tree) gobj_send_event(priv.gobj_nodes_tree, "EV_CLEAR_DATA", {}, gobj);
|
|
18966
19092
|
request_treedb_descs(gobj);
|
|
18967
19093
|
}
|
|
19094
|
+
/********************************************
|
|
19095
|
+
* The last gate before a write leaves for the yuno. The `edition`
|
|
19096
|
+
* mode is gone on a replica, so no button can raise these -- but the
|
|
19097
|
+
* G6 child also publishes them from its undo/redo history and from
|
|
19098
|
+
* saving the node geometry, and a write that got this far would
|
|
19099
|
+
* travel, be refused by the yuno, and come back as a toast with the
|
|
19100
|
+
* graph already redrawn.
|
|
19101
|
+
********************************************/
|
|
19102
|
+
function refuse_if_readonly(gobj, event) {
|
|
19103
|
+
if (!gobj_read_bool_attr(gobj, "readonly")) return false;
|
|
19104
|
+
log_error(`${gobj_short_name(gobj)}: ${event} refused, treedb '${gobj_read_str_attr(gobj, "treedb_name")}' is READ-ONLY`);
|
|
19105
|
+
return true;
|
|
19106
|
+
}
|
|
18968
19107
|
/***************************
|
|
18969
19108
|
* Actions
|
|
18970
19109
|
***************************/
|
|
@@ -19033,7 +19172,15 @@ function ac_mt_command_answer(gobj, event, kw, src) {
|
|
|
19033
19172
|
case "update-node":
|
|
19034
19173
|
case "delete-node":
|
|
19035
19174
|
case "link-nodes":
|
|
19036
|
-
case "unlink-nodes":
|
|
19175
|
+
case "unlink-nodes":
|
|
19176
|
+
if (result >= 0 && kw_get_str(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
|
|
19177
|
+
treedb_name: kw_get_str(gobj, kw_command, "treedb_name", "", 0),
|
|
19178
|
+
topic_name: kw_get_str(gobj, kw_command, "topic_name", "", 0),
|
|
19179
|
+
record: kw_get_dict(gobj, kw_command, "record", {}, 0),
|
|
19180
|
+
created: command === "create-node",
|
|
19181
|
+
command
|
|
19182
|
+
});
|
|
19183
|
+
break;
|
|
19037
19184
|
default: log_error(`${gobj_short_name(gobj)} Command unknown: ${command}`);
|
|
19038
19185
|
}
|
|
19039
19186
|
return 0;
|
|
@@ -19252,7 +19399,9 @@ function ac_edge_clicked(gobj, event, kw, src) {
|
|
|
19252
19399
|
* Send to backend
|
|
19253
19400
|
********************************************/
|
|
19254
19401
|
function ac_create_node(gobj, event, kw, src) {
|
|
19255
|
-
let
|
|
19402
|
+
let priv = gobj.priv;
|
|
19403
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
19404
|
+
let treedb_name = priv.treedb_name;
|
|
19256
19405
|
let topic_name = kw.topic_name;
|
|
19257
19406
|
let record = kw.record;
|
|
19258
19407
|
return treedb_create_node(gobj, treedb_name, topic_name, record, kw.options || {});
|
|
@@ -19267,7 +19416,9 @@ function ac_create_node(gobj, event, kw, src) {
|
|
|
19267
19416
|
* Send to backend
|
|
19268
19417
|
********************************************/
|
|
19269
19418
|
function ac_update_node(gobj, event, kw, src) {
|
|
19270
|
-
let
|
|
19419
|
+
let priv = gobj.priv;
|
|
19420
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
19421
|
+
let treedb_name = priv.treedb_name;
|
|
19271
19422
|
let topic_name = kw.topic_name;
|
|
19272
19423
|
let record = kw.record;
|
|
19273
19424
|
return treedb_update_node(gobj, treedb_name, topic_name, record, kw.options || {});
|
|
@@ -19282,7 +19433,9 @@ function ac_update_node(gobj, event, kw, src) {
|
|
|
19282
19433
|
* Send to backend
|
|
19283
19434
|
********************************************/
|
|
19284
19435
|
function ac_delete_node(gobj, event, kw, src) {
|
|
19285
|
-
let
|
|
19436
|
+
let priv = gobj.priv;
|
|
19437
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
19438
|
+
let treedb_name = priv.treedb_name;
|
|
19286
19439
|
let topic_name = kw.topic_name;
|
|
19287
19440
|
let record = kw.record;
|
|
19288
19441
|
return treedb_delete_node(gobj, treedb_name, topic_name, record, kw.options || {});
|
|
@@ -19297,7 +19450,9 @@ function ac_delete_node(gobj, event, kw, src) {
|
|
|
19297
19450
|
* Send to backend
|
|
19298
19451
|
********************************************/
|
|
19299
19452
|
function ac_link_nodes(gobj, event, kw, src) {
|
|
19300
|
-
let
|
|
19453
|
+
let priv = gobj.priv;
|
|
19454
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
19455
|
+
let treedb_name = priv.treedb_name;
|
|
19301
19456
|
let parent_ref = kw.parent_ref;
|
|
19302
19457
|
let child_ref = kw.child_ref;
|
|
19303
19458
|
return treedb_link_nodes(gobj, treedb_name, parent_ref, child_ref, kw.options || {});
|
|
@@ -19312,7 +19467,9 @@ function ac_link_nodes(gobj, event, kw, src) {
|
|
|
19312
19467
|
* Send to backend
|
|
19313
19468
|
********************************************/
|
|
19314
19469
|
function ac_unlink_nodes(gobj, event, kw, src) {
|
|
19315
|
-
let
|
|
19470
|
+
let priv = gobj.priv;
|
|
19471
|
+
if (refuse_if_readonly(gobj, event)) return -1;
|
|
19472
|
+
let treedb_name = priv.treedb_name;
|
|
19316
19473
|
let parent_ref = kw.parent_ref;
|
|
19317
19474
|
let child_ref = kw.child_ref;
|
|
19318
19475
|
return treedb_unlink_nodes(gobj, treedb_name, parent_ref, child_ref, kw.options || {});
|
|
@@ -19352,6 +19509,10 @@ function ac_set_layout$1(gobj, event, kw, src) {
|
|
|
19352
19509
|
function ac_set_operation_mode$1(gobj, event, kw, src) {
|
|
19353
19510
|
let priv = gobj.priv;
|
|
19354
19511
|
let operation_mode = kw.operation_mode;
|
|
19512
|
+
if (operation_mode === "edition" && gobj_read_bool_attr(gobj, "readonly")) {
|
|
19513
|
+
log_error(`${gobj_short_name(gobj)}: edition mode refused, treedb '${gobj_read_str_attr(gobj, "treedb_name")}' is READ-ONLY`);
|
|
19514
|
+
return -1;
|
|
19515
|
+
}
|
|
19355
19516
|
gobj_write_str_attr(gobj, "operation_mode", operation_mode);
|
|
19356
19517
|
gobj_save_persistent_attrs(gobj, "operation_mode");
|
|
19357
19518
|
let $operation_mode_select = priv.$container.querySelector(".GRAPH_MODE_SELECT");
|
|
@@ -19573,6 +19734,7 @@ function create_gclass$3(gclass_name) {
|
|
|
19573
19734
|
["EV_SET_OPERATION_MODE", 0],
|
|
19574
19735
|
["EV_SET_FOCUS_TOPIC", 0],
|
|
19575
19736
|
["EV_OPERATION_MODE_CHANGED", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
19737
|
+
["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
19576
19738
|
["EV_SHOW", 0],
|
|
19577
19739
|
["EV_HIDE", 0],
|
|
19578
19740
|
["EV_TRANSPORT_STATE", 0]
|
package/package.json
CHANGED
|
@@ -115,6 +115,7 @@ SDATA(data_type_t.DTP_POINTER, "gobj_remote_yuno", 0, null, "Remote Yuno to
|
|
|
115
115
|
SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote service treedb name"),
|
|
116
116
|
SDATA(data_type_t.DTP_DICT, "descs", 0, null, "Descriptions of topics obtained"),
|
|
117
117
|
SDATA(data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
|
|
118
|
+
SDATA(data_type_t.DTP_BOOLEAN, "readonly", 0, false, "The whole treedb is read-only: the graph drops its `edition` mode (the only one that draws write affordances) and refuses every write event. 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`)"),
|
|
118
119
|
SDATA(data_type_t.DTP_STRING, "back_route", 0, "", "Optional hash route for a '← topics' button back to the topics grid (host-supplied; empty = no button)"),
|
|
119
120
|
SDATA(data_type_t.DTP_STRING, "base_route", 0, "", "This view's base route (host-supplied); used to declare its per-topic focus sub-routes to the site map (ROUTING.md contributor)."),
|
|
120
121
|
SDATA(data_type_t.DTP_DICT, "records", 0, "{}", "Data of topics"),
|
|
@@ -193,6 +194,16 @@ function mt_create(gobj)
|
|
|
193
194
|
log_error(`${gobj_name(gobj)} -> treedb_name not configured`);
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
/*
|
|
198
|
+
* `edition` is the ONLY mode that draws write affordances, and the
|
|
199
|
+
* mode is a PERSISTED user preference: a graph that was left in
|
|
200
|
+
* edition on a master would come back in edition on a replica.
|
|
201
|
+
* Fall back to `reading` before the G6 child is born with it.
|
|
202
|
+
*/
|
|
203
|
+
if(gobj_read_bool_attr(gobj, "readonly") && priv.operation_mode === "edition") {
|
|
204
|
+
gobj_write_str_attr(gobj, "operation_mode", "reading");
|
|
205
|
+
}
|
|
206
|
+
|
|
196
207
|
/*
|
|
197
208
|
* set canvas_id, before build_ui()
|
|
198
209
|
*/
|
|
@@ -433,6 +444,11 @@ function make_toolbar(gobj)
|
|
|
433
444
|
* Top Header toolbar
|
|
434
445
|
*---------------------------------------*/
|
|
435
446
|
let modes = gobj_read_attr(gobj, "operation_modes");
|
|
447
|
+
if(gobj_read_bool_attr(gobj, "readonly")) {
|
|
448
|
+
/* A mode the user cannot have is not offered: `edition` is the
|
|
449
|
+
* one that draws the create / delete / link affordances. */
|
|
450
|
+
modes = modes.filter(mode => mode !== "edition");
|
|
451
|
+
}
|
|
436
452
|
let mode_options = modes.map(item => ['option', {}, item]);
|
|
437
453
|
|
|
438
454
|
/*
|
|
@@ -1342,6 +1358,24 @@ function refresh_data(gobj)
|
|
|
1342
1358
|
request_treedb_descs(gobj);
|
|
1343
1359
|
}
|
|
1344
1360
|
|
|
1361
|
+
/********************************************
|
|
1362
|
+
* The last gate before a write leaves for the yuno. The `edition`
|
|
1363
|
+
* mode is gone on a replica, so no button can raise these -- but the
|
|
1364
|
+
* G6 child also publishes them from its undo/redo history and from
|
|
1365
|
+
* saving the node geometry, and a write that got this far would
|
|
1366
|
+
* travel, be refused by the yuno, and come back as a toast with the
|
|
1367
|
+
* graph already redrawn.
|
|
1368
|
+
********************************************/
|
|
1369
|
+
function refuse_if_readonly(gobj, event)
|
|
1370
|
+
{
|
|
1371
|
+
if(!gobj_read_bool_attr(gobj, "readonly")) {
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
log_error(`${gobj_short_name(gobj)}: ${event} refused, treedb ` +
|
|
1375
|
+
`'${gobj_read_str_attr(gobj, "treedb_name")}' is READ-ONLY`);
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1345
1379
|
|
|
1346
1380
|
|
|
1347
1381
|
|
|
@@ -1468,7 +1502,36 @@ function ac_mt_command_answer(gobj, event, kw, src)
|
|
|
1468
1502
|
case "delete-node":
|
|
1469
1503
|
case "link-nodes":
|
|
1470
1504
|
case "unlink-nodes":
|
|
1471
|
-
|
|
1505
|
+
/*
|
|
1506
|
+
* The graph redraws itself from the treedb's own EV_TREEDB_NODE_*
|
|
1507
|
+
* events, which arrive for EVERY writer. This says what those
|
|
1508
|
+
* cannot: THIS view has just written, and the yuno took it. A host
|
|
1509
|
+
* whose save is not finished with the record -- a schema editor,
|
|
1510
|
+
* where the yuno still has to be restarted to re-read the schema --
|
|
1511
|
+
* needs exactly that, and cannot use the node events without
|
|
1512
|
+
* answering its own writes in a loop.
|
|
1513
|
+
*
|
|
1514
|
+
* Same event and same kw as C_YUI_TREEDB_TOPICS, plus `command`:
|
|
1515
|
+
* in a graph a LINK is as much a write as a record is, and the
|
|
1516
|
+
* three of them carry different halves of the kw.
|
|
1517
|
+
*
|
|
1518
|
+
* `__graphs__` is EXCLUDED, and that exclusion is the point of
|
|
1519
|
+
* reading this comment: the view writes that topic ITSELF, one
|
|
1520
|
+
* record per topic, every time the layout is saved. It is this
|
|
1521
|
+
* view's bookkeeping, not the operator's data -- reporting it
|
|
1522
|
+
* would tell a schema editor that the schema changed because
|
|
1523
|
+
* somebody dragged a node.
|
|
1524
|
+
*/
|
|
1525
|
+
if(result >= 0 &&
|
|
1526
|
+
kw_get_str(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") {
|
|
1527
|
+
gobj_publish_event(gobj, "EV_RECORD_WRITTEN", {
|
|
1528
|
+
treedb_name: kw_get_str(gobj, kw_command, "treedb_name", "", 0),
|
|
1529
|
+
topic_name: kw_get_str(gobj, kw_command, "topic_name", "", 0),
|
|
1530
|
+
record: kw_get_dict(gobj, kw_command, "record", {}, 0),
|
|
1531
|
+
created: (command === "create-node"),
|
|
1532
|
+
command: command
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1472
1535
|
break;
|
|
1473
1536
|
|
|
1474
1537
|
default:
|
|
@@ -1787,6 +1850,11 @@ function ac_edge_clicked(gobj, event, kw, src)
|
|
|
1787
1850
|
function ac_create_node(gobj, event, kw, src)
|
|
1788
1851
|
{
|
|
1789
1852
|
let priv = gobj.priv;
|
|
1853
|
+
|
|
1854
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
1855
|
+
return -1;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1790
1858
|
let treedb_name = priv.treedb_name;
|
|
1791
1859
|
let topic_name = kw.topic_name;
|
|
1792
1860
|
let record = kw.record;
|
|
@@ -1813,6 +1881,11 @@ function ac_create_node(gobj, event, kw, src)
|
|
|
1813
1881
|
function ac_update_node(gobj, event, kw, src)
|
|
1814
1882
|
{
|
|
1815
1883
|
let priv = gobj.priv;
|
|
1884
|
+
|
|
1885
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
1886
|
+
return -1;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1816
1889
|
let treedb_name = priv.treedb_name;
|
|
1817
1890
|
let topic_name = kw.topic_name;
|
|
1818
1891
|
let record = kw.record;
|
|
@@ -1839,6 +1912,11 @@ function ac_update_node(gobj, event, kw, src)
|
|
|
1839
1912
|
function ac_delete_node(gobj, event, kw, src)
|
|
1840
1913
|
{
|
|
1841
1914
|
let priv = gobj.priv;
|
|
1915
|
+
|
|
1916
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
1917
|
+
return -1;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1842
1920
|
let treedb_name = priv.treedb_name;
|
|
1843
1921
|
let topic_name = kw.topic_name;
|
|
1844
1922
|
let record = kw.record;
|
|
@@ -1865,6 +1943,11 @@ function ac_delete_node(gobj, event, kw, src)
|
|
|
1865
1943
|
function ac_link_nodes(gobj, event, kw, src)
|
|
1866
1944
|
{
|
|
1867
1945
|
let priv = gobj.priv;
|
|
1946
|
+
|
|
1947
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
1948
|
+
return -1;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1868
1951
|
let treedb_name = priv.treedb_name;
|
|
1869
1952
|
let parent_ref = kw.parent_ref;
|
|
1870
1953
|
let child_ref = kw.child_ref;
|
|
@@ -1891,6 +1974,11 @@ function ac_link_nodes(gobj, event, kw, src)
|
|
|
1891
1974
|
function ac_unlink_nodes(gobj, event, kw, src)
|
|
1892
1975
|
{
|
|
1893
1976
|
let priv = gobj.priv;
|
|
1977
|
+
|
|
1978
|
+
if(refuse_if_readonly(gobj, event)) {
|
|
1979
|
+
return -1;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1894
1982
|
let treedb_name = priv.treedb_name;
|
|
1895
1983
|
let parent_ref = kw.parent_ref;
|
|
1896
1984
|
let child_ref = kw.child_ref;
|
|
@@ -2001,6 +2089,14 @@ function ac_set_operation_mode(gobj, event, kw, src)
|
|
|
2001
2089
|
let priv = gobj.priv;
|
|
2002
2090
|
|
|
2003
2091
|
let operation_mode = kw.operation_mode;
|
|
2092
|
+
if(operation_mode === "edition" && gobj_read_bool_attr(gobj, "readonly")) {
|
|
2093
|
+
/* The select does not offer it on a replica, so getting here means
|
|
2094
|
+
* the event came from somewhere else -- and it is the mode that
|
|
2095
|
+
* turns on every write affordance. */
|
|
2096
|
+
log_error(`${gobj_short_name(gobj)}: edition mode refused, treedb ` +
|
|
2097
|
+
`'${gobj_read_str_attr(gobj, "treedb_name")}' is READ-ONLY`);
|
|
2098
|
+
return -1;
|
|
2099
|
+
}
|
|
2004
2100
|
gobj_write_str_attr(gobj, "operation_mode", operation_mode);
|
|
2005
2101
|
gobj_save_persistent_attrs(gobj, "operation_mode");
|
|
2006
2102
|
|
|
@@ -2168,6 +2264,8 @@ function create_gclass(gclass_name)
|
|
|
2168
2264
|
["EV_SET_FOCUS_TOPIC", 0],
|
|
2169
2265
|
["EV_OPERATION_MODE_CHANGED",
|
|
2170
2266
|
event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
2267
|
+
["EV_RECORD_WRITTEN",
|
|
2268
|
+
event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
|
|
2171
2269
|
["EV_SHOW", 0],
|
|
2172
2270
|
["EV_HIDE", 0],
|
|
2173
2271
|
["EV_TRANSPORT_STATE", 0],
|