@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 CHANGED
@@ -430,6 +430,45 @@ 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` and `C_YUI_TREEDB_GRAPH` take a **`readonly`** attr; the
436
+ topics view propagates it to every topic it builds. It is not one more button
437
+ flag: it is the STATE of the treedb and it beats each `with_*` flag at once,
438
+ because a treedb whose tranger the yuno does not master answers **every** write
439
+ with
440
+
441
+ ```
442
+ ERROR -1: <yuno>: treedb '<name>' is READ-ONLY, this yuno is not the master of its tranger
443
+ ```
444
+
445
+ (the yuno refuses since SDK 7.13.0), so offering the buttons anyway turns a
446
+ fact into an error message per click. Ask the yuno which it is with
447
+ `command-yuno id=<yuno> service=<treedb> command=treedb-info`, which answers
448
+ `{treedb_name, master, schema_version, topics}` — and remember the flag is per
449
+ TREEDB and is runtime state: a yuno is routinely the master of its
450
+ `treedb_system_schema` and a replica of a data treedb it shares.
451
+
452
+ What `readonly` takes away: the edition mode, the *new* / *delete* / *paste*
453
+ buttons, the in-row edit icons, and the write half of the record form's toolbar
454
+ (`copy` stays — reading a record includes taking it with you) with the cells not
455
+ editable. The record form still OPENS: looking is the point of a replica.
456
+
457
+ In the **graph** it takes away the `edition` operation mode, which is the only
458
+ one that draws the create / delete / link affordances — the mode select stops
459
+ offering it, and a graph left in edition on a master comes back in `reading` on
460
+ a replica (the mode is a persisted preference). The other modes are untouched:
461
+ panning, zooming and opening a node are reading.
462
+
463
+ Two implementation notes worth keeping:
464
+
465
+ - the decision lives in **`treedb_write_plan.js`** (pure, tested), not in five
466
+ `!readonly && with_x` expressions — five places to forget the sixth;
467
+ - and the write **events** are refused as well, in every gclass, with a
468
+ `log_error`. Hiding a button is not the same as refusing a write: an event can
469
+ still arrive from a keyboard path or a form that outlived the flag, and an
470
+ ignored write is exactly the behaviour this whole change exists to stop.
471
+
433
472
  ### C_YUI_TREEDB_SCHEMA — the treedb as a graph of topics (prototype)
434
473
 
435
474
  A landing view that draws a treedb as a **graph of topics** — one node per
@@ -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
- with_new_button: true,
15192
- with_delete_button: true,
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$2(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
  ***************************/
@@ -15561,7 +15577,8 @@ function ac_mt_command_answer$1(gobj, event, kw, src) {
15561
15577
  treedb_name: (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "treedb_name", "", 0),
15562
15578
  topic_name: (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0),
15563
15579
  record: (0, _yuneta_gobj_js.kw_get_dict)(gobj, kw_command, "record", {}, 0),
15564
- created: command === "create-node"
15580
+ created: command === "create-node",
15581
+ command
15565
15582
  });
15566
15583
  break;
15567
15584
  case "delete-node": break;
@@ -15766,6 +15783,7 @@ function ac_treedb_node_deleted$1(gobj, event, kw, src) {
15766
15783
  * }
15767
15784
  ********************************************/
15768
15785
  function ac_create_record(gobj, event, kw, src) {
15786
+ if (refuse_if_readonly$2(gobj, event)) return -1;
15769
15787
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
15770
15788
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
15771
15789
  let record = kw.record;
@@ -15783,6 +15801,7 @@ function ac_create_record(gobj, event, kw, src) {
15783
15801
  * }
15784
15802
  ********************************************/
15785
15803
  function ac_update_record(gobj, event, kw, src) {
15804
+ if (refuse_if_readonly$2(gobj, event)) return -1;
15786
15805
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
15787
15806
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
15788
15807
  let record = kw.record;
@@ -15799,6 +15818,7 @@ function ac_update_record(gobj, event, kw, src) {
15799
15818
  * }
15800
15819
  ********************************************/
15801
15820
  function ac_delete_record(gobj, event, kw, src) {
15821
+ if (refuse_if_readonly$2(gobj, event)) return -1;
15802
15822
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
15803
15823
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
15804
15824
  let record = kw.record;
@@ -16086,6 +16106,68 @@ function yui_tabulator_relocalize(table, t) {
16086
16106
  }
16087
16107
  }
16088
16108
  //#endregion
16109
+ //#region src/treedb_write_plan.js
16110
+ /***********************************************************************
16111
+ * treedb_write_plan.js
16112
+ *
16113
+ * Which write affordances a treedb topic offers.
16114
+ *
16115
+ * Pure, so it can be tested without a DOM: the gclass turns the
16116
+ * plan into buttons and columns, this decides the plan. Same split
16117
+ * as form_toolbar_plan.js.
16118
+ *
16119
+ * The rule it exists to state ONCE: `readonly` is not one more
16120
+ * button flag, it is the STATE of the topic and it wins over all of
16121
+ * them. A treedb whose tranger this yuno does not master answers
16122
+ * every write with "READ-ONLY" (the yuno refuses since SDK 7.13.0),
16123
+ * so offering the buttons anyway only turns a fact into an error
16124
+ * message per click. Written as `!readonly && with_x` in five
16125
+ * places it is five places to forget the sixth.
16126
+ *
16127
+ * Copyright (c) 2026, ArtGins.
16128
+ * All Rights Reserved.
16129
+ ***********************************************************************/
16130
+ var READONLY_FORM_TOOLBAR = ["copy"];
16131
+ /***************************************************************
16132
+ * plan_treedb_writes(flags) -> plan
16133
+ *
16134
+ * flags {readonly, with_edition_mode, with_new_button,
16135
+ * with_delete_button, with_paste_button,
16136
+ * with_in_row_edit_icons}
16137
+ * Every with_* defaults to TRUE when absent, which is
16138
+ * what the gclass attrs default to: a caller that sets
16139
+ * nothing gets the toolbar this view always had.
16140
+ *
16141
+ * plan the same names without the `with_` prefix, plus
16142
+ * `form_toolbar`: the list to hand C_YUI_FORM, or null
16143
+ * to leave the form's own default alone.
16144
+ ***************************************************************/
16145
+ function plan_treedb_writes(flags) {
16146
+ let f = flags || {};
16147
+ let readonly = !!f.readonly;
16148
+ let want = (name) => {
16149
+ return f[name] === void 0 ? true : !!f[name];
16150
+ };
16151
+ if (readonly) return {
16152
+ readonly: true,
16153
+ edition_mode: false,
16154
+ new_button: false,
16155
+ delete_button: false,
16156
+ paste_button: false,
16157
+ in_row_icons: false,
16158
+ form_toolbar: READONLY_FORM_TOOLBAR.slice()
16159
+ };
16160
+ return {
16161
+ readonly: false,
16162
+ edition_mode: want("with_edition_mode"),
16163
+ new_button: want("with_new_button"),
16164
+ delete_button: want("with_delete_button"),
16165
+ paste_button: want("with_paste_button"),
16166
+ in_row_icons: want("with_in_row_edit_icons"),
16167
+ form_toolbar: null
16168
+ };
16169
+ }
16170
+ //#endregion
16089
16171
  //#region src/c_yui_treedb_topic_with_form.js
16090
16172
  /***********************************************************************
16091
16173
  * c_yui_treedb_topic_with_form.js
@@ -16107,6 +16189,7 @@ var attrs_table$4 = [
16107
16189
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote service treedb name"),
16108
16190
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "topic_name", 0, null, "Topic name"),
16109
16191
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_POINTER, "desc", 0, null, "Description of topics"),
16192
+ (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
16193
  (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
16194
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_new_button", 0, true, "Button toolbar NEW"),
16112
16195
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_delete_button", 0, true, "Button toolbar DELETE"),
@@ -16269,11 +16352,12 @@ async function readClipboard(gobj) {
16269
16352
  function build_ui$4(gobj) {
16270
16353
  function create_table_toolbar() {
16271
16354
  let $table_toolbar = [];
16272
- let with_edition_mode = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_edition_mode");
16273
- let with_new_button = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_new_button");
16274
- let with_delete_button = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_delete_button");
16355
+ let plan = write_plan(gobj);
16356
+ let with_edition_mode = plan.edition_mode;
16357
+ let with_new_button = plan.new_button;
16358
+ let with_delete_button = plan.delete_button;
16275
16359
  let with_copy_button = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_copy_button");
16276
- let with_paste_button = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_paste_button");
16360
+ let with_paste_button = plan.paste_button;
16277
16361
  let toolbar_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "toolbar_id");
16278
16362
  if (with_edition_mode) {
16279
16363
  $table_toolbar = (0, _yuneta_gobj_js.createElement2)(["div", {
@@ -16716,7 +16800,7 @@ function create_tabulator(gobj) {
16716
16800
  "</button>"
16717
16801
  ].join("");
16718
16802
  }
16719
- if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_in_row_edit_icons")) columns.push({
16803
+ if (write_plan(gobj).in_row_icons) columns.push({
16720
16804
  field: "_operation",
16721
16805
  title: "Op",
16722
16806
  hozAlign: "center",
@@ -16995,15 +17079,17 @@ function open_form_dialog(gobj, mode, record) {
16995
17079
  },
16996
17080
  []
16997
17081
  ]);
17082
+ let form_plan = write_plan(gobj);
16998
17083
  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
17084
  template: build_form_template(gobj),
17000
17085
  record,
17001
17086
  fkey_options: build_fkey_options(gobj),
17002
17087
  form_mode: mode,
17088
+ ...form_plan.form_toolbar ? { toolbar: form_plan.form_toolbar } : {},
17003
17089
  render_mode: "edit",
17004
17090
  pkey: desc.pkey || "id",
17005
17091
  topic_name,
17006
- editable: true,
17092
+ editable: !form_plan.readonly,
17007
17093
  $parent: $body
17008
17094
  }, gobj);
17009
17095
  priv.form = form;
@@ -17530,6 +17616,34 @@ function show_dropdown_popup_menu(gobj, x, y, items, callback) {
17530
17616
  let $with_focus = $element.querySelector(".with-focus");
17531
17617
  if ($with_focus) $with_focus.focus();
17532
17618
  }
17619
+ /***************************************************************
17620
+ * A read-only topic refuses the write EVENTS too, not only their
17621
+ * buttons. The buttons are gone, but an event can still arrive: a
17622
+ * keyboard path, a hosted form that outlived the flag, a caller that
17623
+ * sends EV_SAVE_RECORD itself. Refusing here is what makes `readonly`
17624
+ * a state of the gclass rather than a way of drawing it -- and it
17625
+ * fails LOUDLY, because an ignored write is how the backend used to
17626
+ * behave and what this whole change exists to stop.
17627
+ ***************************************************************/
17628
+ function write_plan(gobj) {
17629
+ return plan_treedb_writes({
17630
+ readonly: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly"),
17631
+ with_edition_mode: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_edition_mode"),
17632
+ with_new_button: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_new_button"),
17633
+ with_delete_button: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_delete_button"),
17634
+ with_paste_button: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_paste_button"),
17635
+ with_in_row_edit_icons: (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_in_row_edit_icons")
17636
+ });
17637
+ }
17638
+ /***************************************************************
17639
+ * A read-only topic refuses the write EVENTS too, not only their
17640
+ * buttons.
17641
+ ***************************************************************/
17642
+ function refuse_if_readonly$1(gobj, event) {
17643
+ if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly")) return false;
17644
+ (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`);
17645
+ return true;
17646
+ }
17533
17647
  /***************************
17534
17648
  * Actions
17535
17649
  ***************************/
@@ -17597,7 +17711,10 @@ function ac_load_node_updated(gobj, event, kw, src) {
17597
17711
  return -1;
17598
17712
  }
17599
17713
  let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
17600
- if (tabulator) for (let record of data) tabulator.updateData([record]);
17714
+ if (tabulator) {
17715
+ for (let record of data) if (tabulator.getRow(record.id)) tabulator.updateData([record]);
17716
+ else if (tabulator.getDataCount() > 0) (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: updated node is not in the loaded table: ${record.id}`);
17717
+ }
17601
17718
  return 0;
17602
17719
  }
17603
17720
  /************************************************************
@@ -17622,6 +17739,7 @@ function ac_node_deleted$1(gobj, event, kw, src) {
17622
17739
  * and the Op column is visible (with row's edit/delete icons)
17623
17740
  ************************************************************/
17624
17741
  function ac_edition_mode(gobj, event, kw, src) {
17742
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17625
17743
  let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
17626
17744
  let editable = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable");
17627
17745
  editable = !editable;
@@ -17665,6 +17783,7 @@ function ac_edition_mode(gobj, event, kw, src) {
17665
17783
  * - the backend will broadcast the new record that will be added to table
17666
17784
  ************************************************************/
17667
17785
  function ac_new_row(gobj, event, kw, src) {
17786
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17668
17787
  let row = {};
17669
17788
  let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
17670
17789
  for (let i = 0; i < desc.cols.length; i++) {
@@ -17694,6 +17813,7 @@ function ac_new_row(gobj, event, kw, src) {
17694
17813
  * kw {index:, row:}
17695
17814
  ************************************************************/
17696
17815
  function ac_delete_rows(gobj, event, kw, src) {
17816
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17697
17817
  let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
17698
17818
  if ((0, _yuneta_gobj_js.json_size)(kw) === 0) {
17699
17819
  let rows = tabulator.getSelectedData();
@@ -17757,6 +17877,7 @@ function ac_copy_rows(gobj, event, kw, src) {
17757
17877
  * From internal
17758
17878
  ************************************************************/
17759
17879
  function ac_paste_rows(gobj, event, kw, src) {
17880
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17760
17881
  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
17882
  topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
17762
17883
  record: row
@@ -17769,6 +17890,7 @@ function ac_paste_rows(gobj, event, kw, src) {
17769
17890
  * fkeys, times and numbers). Route by the form's mode.
17770
17891
  ************************************************************/
17771
17892
  function ac_form_save_record(gobj, event, kw, src) {
17893
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17772
17894
  let mode = (0, _yuneta_gobj_js.gobj_read_str_attr)(src, "form_mode");
17773
17895
  (0, _yuneta_gobj_js.gobj_publish_event)(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", {
17774
17896
  topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
@@ -18184,6 +18306,7 @@ var attrs_table$3 = [
18184
18306
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote service treedb name"),
18185
18307
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_DICT, "descs", 0, null, "Descriptions of topics obtained"),
18186
18308
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "system", 0, false, "Manage system topics (true) or user topics (false)"),
18309
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.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`)"),
18187
18310
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.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)"),
18188
18311
  (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 per-topic focus sub-routes to the site map (ROUTING.md contributor)."),
18189
18312
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_DICT, "records", 0, "{}", "Data of topics"),
@@ -18234,6 +18357,7 @@ function mt_create$3(gobj) {
18234
18357
  if (!subscriber) subscriber = (0, _yuneta_gobj_js.gobj_parent)(gobj);
18235
18358
  (0, _yuneta_gobj_js.gobj_subscribe_event)(gobj, null, {}, subscriber);
18236
18359
  if (!priv.treedb_name) (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_name)(gobj)} -> treedb_name not configured`);
18360
+ if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly") && priv.operation_mode === "edition") (0, _yuneta_gobj_js.gobj_write_str_attr)(gobj, "operation_mode", "reading");
18237
18361
  let canvas_id = (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj)) + "-canvas";
18238
18362
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "canvas_id", canvas_id);
18239
18363
  let $container_canvas = build_ui$3(gobj).querySelector(`#${priv.canvas_id}`);
@@ -18388,6 +18512,8 @@ function destroy_ui(gobj) {
18388
18512
  ************************************************************/
18389
18513
  function make_toolbar(gobj) {
18390
18514
  gobj.priv;
18515
+ let modes = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "operation_modes");
18516
+ if ((0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly")) modes = modes.filter((mode) => mode !== "edition");
18391
18517
  const $toolbar_header = yui_toolbar({}, [
18392
18518
  [
18393
18519
  "div",
@@ -18426,7 +18552,7 @@ function make_toolbar(gobj) {
18426
18552
  [[
18427
18553
  "select",
18428
18554
  { class: "GRAPH_MODE_SELECT" },
18429
- (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "operation_modes").map((item) => [
18555
+ modes.map((item) => [
18430
18556
  "option",
18431
18557
  {},
18432
18558
  item
@@ -18978,6 +19104,19 @@ function refresh_data(gobj) {
18978
19104
  if (priv.gobj_nodes_tree) (0, _yuneta_gobj_js.gobj_send_event)(priv.gobj_nodes_tree, "EV_CLEAR_DATA", {}, gobj);
18979
19105
  request_treedb_descs(gobj);
18980
19106
  }
19107
+ /********************************************
19108
+ * The last gate before a write leaves for the yuno. The `edition`
19109
+ * mode is gone on a replica, so no button can raise these -- but the
19110
+ * G6 child also publishes them from its undo/redo history and from
19111
+ * saving the node geometry, and a write that got this far would
19112
+ * travel, be refused by the yuno, and come back as a toast with the
19113
+ * graph already redrawn.
19114
+ ********************************************/
19115
+ function refuse_if_readonly(gobj, event) {
19116
+ if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly")) return false;
19117
+ (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`);
19118
+ return true;
19119
+ }
18981
19120
  /***************************
18982
19121
  * Actions
18983
19122
  ***************************/
@@ -19046,7 +19185,15 @@ function ac_mt_command_answer(gobj, event, kw, src) {
19046
19185
  case "update-node":
19047
19186
  case "delete-node":
19048
19187
  case "link-nodes":
19049
- case "unlink-nodes": break;
19188
+ case "unlink-nodes":
19189
+ if (result >= 0 && (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0) !== "__graphs__") (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_RECORD_WRITTEN", {
19190
+ treedb_name: (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "treedb_name", "", 0),
19191
+ topic_name: (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0),
19192
+ record: (0, _yuneta_gobj_js.kw_get_dict)(gobj, kw_command, "record", {}, 0),
19193
+ created: command === "create-node",
19194
+ command
19195
+ });
19196
+ break;
19050
19197
  default: (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)} Command unknown: ${command}`);
19051
19198
  }
19052
19199
  return 0;
@@ -19265,7 +19412,9 @@ function ac_edge_clicked(gobj, event, kw, src) {
19265
19412
  * Send to backend
19266
19413
  ********************************************/
19267
19414
  function ac_create_node(gobj, event, kw, src) {
19268
- let treedb_name = gobj.priv.treedb_name;
19415
+ let priv = gobj.priv;
19416
+ if (refuse_if_readonly(gobj, event)) return -1;
19417
+ let treedb_name = priv.treedb_name;
19269
19418
  let topic_name = kw.topic_name;
19270
19419
  let record = kw.record;
19271
19420
  return treedb_create_node(gobj, treedb_name, topic_name, record, kw.options || {});
@@ -19280,7 +19429,9 @@ function ac_create_node(gobj, event, kw, src) {
19280
19429
  * Send to backend
19281
19430
  ********************************************/
19282
19431
  function ac_update_node(gobj, event, kw, src) {
19283
- let treedb_name = gobj.priv.treedb_name;
19432
+ let priv = gobj.priv;
19433
+ if (refuse_if_readonly(gobj, event)) return -1;
19434
+ let treedb_name = priv.treedb_name;
19284
19435
  let topic_name = kw.topic_name;
19285
19436
  let record = kw.record;
19286
19437
  return treedb_update_node(gobj, treedb_name, topic_name, record, kw.options || {});
@@ -19295,7 +19446,9 @@ function ac_update_node(gobj, event, kw, src) {
19295
19446
  * Send to backend
19296
19447
  ********************************************/
19297
19448
  function ac_delete_node(gobj, event, kw, src) {
19298
- let treedb_name = gobj.priv.treedb_name;
19449
+ let priv = gobj.priv;
19450
+ if (refuse_if_readonly(gobj, event)) return -1;
19451
+ let treedb_name = priv.treedb_name;
19299
19452
  let topic_name = kw.topic_name;
19300
19453
  let record = kw.record;
19301
19454
  return treedb_delete_node(gobj, treedb_name, topic_name, record, kw.options || {});
@@ -19310,7 +19463,9 @@ function ac_delete_node(gobj, event, kw, src) {
19310
19463
  * Send to backend
19311
19464
  ********************************************/
19312
19465
  function ac_link_nodes(gobj, event, kw, src) {
19313
- let treedb_name = gobj.priv.treedb_name;
19466
+ let priv = gobj.priv;
19467
+ if (refuse_if_readonly(gobj, event)) return -1;
19468
+ let treedb_name = priv.treedb_name;
19314
19469
  let parent_ref = kw.parent_ref;
19315
19470
  let child_ref = kw.child_ref;
19316
19471
  return treedb_link_nodes(gobj, treedb_name, parent_ref, child_ref, kw.options || {});
@@ -19325,7 +19480,9 @@ function ac_link_nodes(gobj, event, kw, src) {
19325
19480
  * Send to backend
19326
19481
  ********************************************/
19327
19482
  function ac_unlink_nodes(gobj, event, kw, src) {
19328
- let treedb_name = gobj.priv.treedb_name;
19483
+ let priv = gobj.priv;
19484
+ if (refuse_if_readonly(gobj, event)) return -1;
19485
+ let treedb_name = priv.treedb_name;
19329
19486
  let parent_ref = kw.parent_ref;
19330
19487
  let child_ref = kw.child_ref;
19331
19488
  return treedb_unlink_nodes(gobj, treedb_name, parent_ref, child_ref, kw.options || {});
@@ -19365,6 +19522,10 @@ function ac_set_layout$1(gobj, event, kw, src) {
19365
19522
  function ac_set_operation_mode$1(gobj, event, kw, src) {
19366
19523
  let priv = gobj.priv;
19367
19524
  let operation_mode = kw.operation_mode;
19525
+ if (operation_mode === "edition" && (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly")) {
19526
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: edition mode refused, treedb '${(0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name")}' is READ-ONLY`);
19527
+ return -1;
19528
+ }
19368
19529
  (0, _yuneta_gobj_js.gobj_write_str_attr)(gobj, "operation_mode", operation_mode);
19369
19530
  (0, _yuneta_gobj_js.gobj_save_persistent_attrs)(gobj, "operation_mode");
19370
19531
  let $operation_mode_select = priv.$container.querySelector(".GRAPH_MODE_SELECT");
@@ -19586,6 +19747,7 @@ function create_gclass$3(gclass_name) {
19586
19747
  ["EV_SET_OPERATION_MODE", 0],
19587
19748
  ["EV_SET_FOCUS_TOPIC", 0],
19588
19749
  ["EV_OPERATION_MODE_CHANGED", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
19750
+ ["EV_RECORD_WRITTEN", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
19589
19751
  ["EV_SHOW", 0],
19590
19752
  ["EV_HIDE", 0],
19591
19753
  ["EV_TRANSPORT_STATE", 0]