@yuneta/gobj-ui 5.15.0 → 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
@@ -432,10 +432,11 @@ Logical DOM classes: `JSON_VIEWER`, `JSON_TOOLBAR`, `JSON_SEARCH`, `JSON_TREE`,
432
432
 
433
433
  ### Read-only treedbs: `readonly`
434
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
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
439
440
 
440
441
  ```
441
442
  ERROR -1: <yuno>: treedb '<name>' is READ-ONLY, this yuno is not the master of its tranger
@@ -453,11 +454,17 @@ buttons, the in-row edit icons, and the write half of the record form's toolbar
453
454
  (`copy` stays — reading a record includes taking it with you) with the cells not
454
455
  editable. The record form still OPENS: looking is the point of a replica.
455
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
+
456
463
  Two implementation notes worth keeping:
457
464
 
458
465
  - the decision lives in **`treedb_write_plan.js`** (pure, tested), not in five
459
466
  `!readonly && with_x` expressions — five places to forget the sixth;
460
- - and the write **events** are refused as well, in both gclasses, with a
467
+ - and the write **events** are refused as well, in every gclass, with a
461
468
  `log_error`. Hiding a button is not the same as refusing a write: an event can
462
469
  still arrive from a keyboard path or a form that outlived the flag, and an
463
470
  ignored write is exactly the behaviour this whole change exists to stop.
@@ -15505,7 +15505,7 @@ function request_print_tranger$1(gobj, path) {
15505
15505
  * would travel, be refused by the yuno, and come back as a toast --
15506
15506
  * after the table had already drawn the row.
15507
15507
  ***************************************************************/
15508
- function refuse_if_readonly$1(gobj, event) {
15508
+ function refuse_if_readonly$2(gobj, event) {
15509
15509
  if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly")) return false;
15510
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
15511
  return true;
@@ -15577,7 +15577,8 @@ function ac_mt_command_answer$1(gobj, event, kw, src) {
15577
15577
  treedb_name: (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "treedb_name", "", 0),
15578
15578
  topic_name: (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0),
15579
15579
  record: (0, _yuneta_gobj_js.kw_get_dict)(gobj, kw_command, "record", {}, 0),
15580
- created: command === "create-node"
15580
+ created: command === "create-node",
15581
+ command
15581
15582
  });
15582
15583
  break;
15583
15584
  case "delete-node": break;
@@ -15782,7 +15783,7 @@ function ac_treedb_node_deleted$1(gobj, event, kw, src) {
15782
15783
  * }
15783
15784
  ********************************************/
15784
15785
  function ac_create_record(gobj, event, kw, src) {
15785
- if (refuse_if_readonly$1(gobj, event)) return -1;
15786
+ if (refuse_if_readonly$2(gobj, event)) return -1;
15786
15787
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
15787
15788
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
15788
15789
  let record = kw.record;
@@ -15800,7 +15801,7 @@ function ac_create_record(gobj, event, kw, src) {
15800
15801
  * }
15801
15802
  ********************************************/
15802
15803
  function ac_update_record(gobj, event, kw, src) {
15803
- if (refuse_if_readonly$1(gobj, event)) return -1;
15804
+ if (refuse_if_readonly$2(gobj, event)) return -1;
15804
15805
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
15805
15806
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
15806
15807
  let record = kw.record;
@@ -15817,7 +15818,7 @@ function ac_update_record(gobj, event, kw, src) {
15817
15818
  * }
15818
15819
  ********************************************/
15819
15820
  function ac_delete_record(gobj, event, kw, src) {
15820
- if (refuse_if_readonly$1(gobj, event)) return -1;
15821
+ if (refuse_if_readonly$2(gobj, event)) return -1;
15821
15822
  let treedb_name = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name");
15822
15823
  let topic_name = (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
15823
15824
  let record = kw.record;
@@ -17638,7 +17639,7 @@ function write_plan(gobj) {
17638
17639
  * A read-only topic refuses the write EVENTS too, not only their
17639
17640
  * buttons.
17640
17641
  ***************************************************************/
17641
- function refuse_if_readonly(gobj, event) {
17642
+ function refuse_if_readonly$1(gobj, event) {
17642
17643
  if (!(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly")) return false;
17643
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`);
17644
17645
  return true;
@@ -17710,7 +17711,10 @@ function ac_load_node_updated(gobj, event, kw, src) {
17710
17711
  return -1;
17711
17712
  }
17712
17713
  let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
17713
- 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
+ }
17714
17718
  return 0;
17715
17719
  }
17716
17720
  /************************************************************
@@ -17735,7 +17739,7 @@ function ac_node_deleted$1(gobj, event, kw, src) {
17735
17739
  * and the Op column is visible (with row's edit/delete icons)
17736
17740
  ************************************************************/
17737
17741
  function ac_edition_mode(gobj, event, kw, src) {
17738
- if (refuse_if_readonly(gobj, event)) return -1;
17742
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17739
17743
  let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
17740
17744
  let editable = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable");
17741
17745
  editable = !editable;
@@ -17779,7 +17783,7 @@ function ac_edition_mode(gobj, event, kw, src) {
17779
17783
  * - the backend will broadcast the new record that will be added to table
17780
17784
  ************************************************************/
17781
17785
  function ac_new_row(gobj, event, kw, src) {
17782
- if (refuse_if_readonly(gobj, event)) return -1;
17786
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17783
17787
  let row = {};
17784
17788
  let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
17785
17789
  for (let i = 0; i < desc.cols.length; i++) {
@@ -17809,7 +17813,7 @@ function ac_new_row(gobj, event, kw, src) {
17809
17813
  * kw {index:, row:}
17810
17814
  ************************************************************/
17811
17815
  function ac_delete_rows(gobj, event, kw, src) {
17812
- if (refuse_if_readonly(gobj, event)) return -1;
17816
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17813
17817
  let tabulator = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "tabulator");
17814
17818
  if ((0, _yuneta_gobj_js.json_size)(kw) === 0) {
17815
17819
  let rows = tabulator.getSelectedData();
@@ -17873,7 +17877,7 @@ function ac_copy_rows(gobj, event, kw, src) {
17873
17877
  * From internal
17874
17878
  ************************************************************/
17875
17879
  function ac_paste_rows(gobj, event, kw, src) {
17876
- if (refuse_if_readonly(gobj, event)) return -1;
17880
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17877
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", {
17878
17882
  topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
17879
17883
  record: row
@@ -17886,7 +17890,7 @@ function ac_paste_rows(gobj, event, kw, src) {
17886
17890
  * fkeys, times and numbers). Route by the form's mode.
17887
17891
  ************************************************************/
17888
17892
  function ac_form_save_record(gobj, event, kw, src) {
17889
- if (refuse_if_readonly(gobj, event)) return -1;
17893
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17890
17894
  let mode = (0, _yuneta_gobj_js.gobj_read_str_attr)(src, "form_mode");
17891
17895
  (0, _yuneta_gobj_js.gobj_publish_event)(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", {
17892
17896
  topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
@@ -18302,6 +18306,7 @@ var attrs_table$3 = [
18302
18306
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote service treedb name"),
18303
18307
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_DICT, "descs", 0, null, "Descriptions of topics obtained"),
18304
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`)"),
18305
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)"),
18306
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)."),
18307
18312
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_DICT, "records", 0, "{}", "Data of topics"),
@@ -18352,6 +18357,7 @@ function mt_create$3(gobj) {
18352
18357
  if (!subscriber) subscriber = (0, _yuneta_gobj_js.gobj_parent)(gobj);
18353
18358
  (0, _yuneta_gobj_js.gobj_subscribe_event)(gobj, null, {}, subscriber);
18354
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");
18355
18361
  let canvas_id = (0, _yuneta_gobj_js.clean_name)((0, _yuneta_gobj_js.gobj_name)(gobj)) + "-canvas";
18356
18362
  (0, _yuneta_gobj_js.gobj_write_attr)(gobj, "canvas_id", canvas_id);
18357
18363
  let $container_canvas = build_ui$3(gobj).querySelector(`#${priv.canvas_id}`);
@@ -18506,6 +18512,8 @@ function destroy_ui(gobj) {
18506
18512
  ************************************************************/
18507
18513
  function make_toolbar(gobj) {
18508
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");
18509
18517
  const $toolbar_header = yui_toolbar({}, [
18510
18518
  [
18511
18519
  "div",
@@ -18544,7 +18552,7 @@ function make_toolbar(gobj) {
18544
18552
  [[
18545
18553
  "select",
18546
18554
  { class: "GRAPH_MODE_SELECT" },
18547
- (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "operation_modes").map((item) => [
18555
+ modes.map((item) => [
18548
18556
  "option",
18549
18557
  {},
18550
18558
  item
@@ -19096,6 +19104,19 @@ function refresh_data(gobj) {
19096
19104
  if (priv.gobj_nodes_tree) (0, _yuneta_gobj_js.gobj_send_event)(priv.gobj_nodes_tree, "EV_CLEAR_DATA", {}, gobj);
19097
19105
  request_treedb_descs(gobj);
19098
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
+ }
19099
19120
  /***************************
19100
19121
  * Actions
19101
19122
  ***************************/
@@ -19164,7 +19185,15 @@ function ac_mt_command_answer(gobj, event, kw, src) {
19164
19185
  case "update-node":
19165
19186
  case "delete-node":
19166
19187
  case "link-nodes":
19167
- 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;
19168
19197
  default: (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)} Command unknown: ${command}`);
19169
19198
  }
19170
19199
  return 0;
@@ -19383,7 +19412,9 @@ function ac_edge_clicked(gobj, event, kw, src) {
19383
19412
  * Send to backend
19384
19413
  ********************************************/
19385
19414
  function ac_create_node(gobj, event, kw, src) {
19386
- 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;
19387
19418
  let topic_name = kw.topic_name;
19388
19419
  let record = kw.record;
19389
19420
  return treedb_create_node(gobj, treedb_name, topic_name, record, kw.options || {});
@@ -19398,7 +19429,9 @@ function ac_create_node(gobj, event, kw, src) {
19398
19429
  * Send to backend
19399
19430
  ********************************************/
19400
19431
  function ac_update_node(gobj, event, kw, src) {
19401
- 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;
19402
19435
  let topic_name = kw.topic_name;
19403
19436
  let record = kw.record;
19404
19437
  return treedb_update_node(gobj, treedb_name, topic_name, record, kw.options || {});
@@ -19413,7 +19446,9 @@ function ac_update_node(gobj, event, kw, src) {
19413
19446
  * Send to backend
19414
19447
  ********************************************/
19415
19448
  function ac_delete_node(gobj, event, kw, src) {
19416
- 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;
19417
19452
  let topic_name = kw.topic_name;
19418
19453
  let record = kw.record;
19419
19454
  return treedb_delete_node(gobj, treedb_name, topic_name, record, kw.options || {});
@@ -19428,7 +19463,9 @@ function ac_delete_node(gobj, event, kw, src) {
19428
19463
  * Send to backend
19429
19464
  ********************************************/
19430
19465
  function ac_link_nodes(gobj, event, kw, src) {
19431
- 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;
19432
19469
  let parent_ref = kw.parent_ref;
19433
19470
  let child_ref = kw.child_ref;
19434
19471
  return treedb_link_nodes(gobj, treedb_name, parent_ref, child_ref, kw.options || {});
@@ -19443,7 +19480,9 @@ function ac_link_nodes(gobj, event, kw, src) {
19443
19480
  * Send to backend
19444
19481
  ********************************************/
19445
19482
  function ac_unlink_nodes(gobj, event, kw, src) {
19446
- 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;
19447
19486
  let parent_ref = kw.parent_ref;
19448
19487
  let child_ref = kw.child_ref;
19449
19488
  return treedb_unlink_nodes(gobj, treedb_name, parent_ref, child_ref, kw.options || {});
@@ -19483,6 +19522,10 @@ function ac_set_layout$1(gobj, event, kw, src) {
19483
19522
  function ac_set_operation_mode$1(gobj, event, kw, src) {
19484
19523
  let priv = gobj.priv;
19485
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
+ }
19486
19529
  (0, _yuneta_gobj_js.gobj_write_str_attr)(gobj, "operation_mode", operation_mode);
19487
19530
  (0, _yuneta_gobj_js.gobj_save_persistent_attrs)(gobj, "operation_mode");
19488
19531
  let $operation_mode_select = priv.$container.querySelector(".GRAPH_MODE_SELECT");
@@ -19704,6 +19747,7 @@ function create_gclass$3(gclass_name) {
19704
19747
  ["EV_SET_OPERATION_MODE", 0],
19705
19748
  ["EV_SET_FOCUS_TOPIC", 0],
19706
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],
19707
19751
  ["EV_SHOW", 0],
19708
19752
  ["EV_HIDE", 0],
19709
19753
  ["EV_TRANSPORT_STATE", 0]
@@ -15500,7 +15500,7 @@ function request_print_tranger$1(gobj, path) {
15500
15500
  * would travel, be refused by the yuno, and come back as a toast --
15501
15501
  * after the table had already drawn the row.
15502
15502
  ***************************************************************/
15503
- function refuse_if_readonly$1(gobj, event) {
15503
+ function refuse_if_readonly$2(gobj, event) {
15504
15504
  if (!gobj_read_bool_attr(gobj, "readonly")) return false;
15505
15505
  log_error(`${gobj_short_name(gobj)}: ${event} refused, treedb '${gobj_read_str_attr(gobj, "treedb_name")}' is READ-ONLY`);
15506
15506
  return true;
@@ -15572,7 +15572,8 @@ function ac_mt_command_answer$1(gobj, event, kw, src) {
15572
15572
  treedb_name: kw_get_str(gobj, kw_command, "treedb_name", "", 0),
15573
15573
  topic_name: kw_get_str(gobj, kw_command, "topic_name", "", 0),
15574
15574
  record: kw_get_dict(gobj, kw_command, "record", {}, 0),
15575
- created: command === "create-node"
15575
+ created: command === "create-node",
15576
+ command
15576
15577
  });
15577
15578
  break;
15578
15579
  case "delete-node": break;
@@ -15777,7 +15778,7 @@ function ac_treedb_node_deleted$1(gobj, event, kw, src) {
15777
15778
  * }
15778
15779
  ********************************************/
15779
15780
  function ac_create_record(gobj, event, kw, src) {
15780
- if (refuse_if_readonly$1(gobj, event)) return -1;
15781
+ if (refuse_if_readonly$2(gobj, event)) return -1;
15781
15782
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
15782
15783
  let topic_name = gobj_read_attr(src, "topic_name");
15783
15784
  let record = kw.record;
@@ -15795,7 +15796,7 @@ function ac_create_record(gobj, event, kw, src) {
15795
15796
  * }
15796
15797
  ********************************************/
15797
15798
  function ac_update_record(gobj, event, kw, src) {
15798
- if (refuse_if_readonly$1(gobj, event)) return -1;
15799
+ if (refuse_if_readonly$2(gobj, event)) return -1;
15799
15800
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
15800
15801
  let topic_name = gobj_read_attr(src, "topic_name");
15801
15802
  let record = kw.record;
@@ -15812,7 +15813,7 @@ function ac_update_record(gobj, event, kw, src) {
15812
15813
  * }
15813
15814
  ********************************************/
15814
15815
  function ac_delete_record(gobj, event, kw, src) {
15815
- if (refuse_if_readonly$1(gobj, event)) return -1;
15816
+ if (refuse_if_readonly$2(gobj, event)) return -1;
15816
15817
  let treedb_name = gobj_read_str_attr(gobj, "treedb_name");
15817
15818
  let topic_name = gobj_read_attr(src, "topic_name");
15818
15819
  let record = kw.record;
@@ -17633,7 +17634,7 @@ function write_plan(gobj) {
17633
17634
  * A read-only topic refuses the write EVENTS too, not only their
17634
17635
  * buttons.
17635
17636
  ***************************************************************/
17636
- function refuse_if_readonly(gobj, event) {
17637
+ function refuse_if_readonly$1(gobj, event) {
17637
17638
  if (!gobj_read_bool_attr(gobj, "readonly")) return false;
17638
17639
  log_error(`${gobj_short_name(gobj)}: ${event} refused, topic '${gobj_read_str_attr(gobj, "topic_name")}' is READ-ONLY`);
17639
17640
  return true;
@@ -17705,7 +17706,10 @@ function ac_load_node_updated(gobj, event, kw, src) {
17705
17706
  return -1;
17706
17707
  }
17707
17708
  let tabulator = gobj_read_attr(gobj, "tabulator");
17708
- if (tabulator) for (let record of data) tabulator.updateData([record]);
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
+ }
17709
17713
  return 0;
17710
17714
  }
17711
17715
  /************************************************************
@@ -17730,7 +17734,7 @@ function ac_node_deleted$1(gobj, event, kw, src) {
17730
17734
  * and the Op column is visible (with row's edit/delete icons)
17731
17735
  ************************************************************/
17732
17736
  function ac_edition_mode(gobj, event, kw, src) {
17733
- if (refuse_if_readonly(gobj, event)) return -1;
17737
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17734
17738
  let tabulator = gobj_read_attr(gobj, "tabulator");
17735
17739
  let editable = gobj_read_bool_attr(gobj, "editable");
17736
17740
  editable = !editable;
@@ -17774,7 +17778,7 @@ function ac_edition_mode(gobj, event, kw, src) {
17774
17778
  * - the backend will broadcast the new record that will be added to table
17775
17779
  ************************************************************/
17776
17780
  function ac_new_row(gobj, event, kw, src) {
17777
- if (refuse_if_readonly(gobj, event)) return -1;
17781
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17778
17782
  let row = {};
17779
17783
  let desc = gobj_read_attr(gobj, "desc");
17780
17784
  for (let i = 0; i < desc.cols.length; i++) {
@@ -17804,7 +17808,7 @@ function ac_new_row(gobj, event, kw, src) {
17804
17808
  * kw {index:, row:}
17805
17809
  ************************************************************/
17806
17810
  function ac_delete_rows(gobj, event, kw, src) {
17807
- if (refuse_if_readonly(gobj, event)) return -1;
17811
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17808
17812
  let tabulator = gobj_read_attr(gobj, "tabulator");
17809
17813
  if (json_size(kw) === 0) {
17810
17814
  let rows = tabulator.getSelectedData();
@@ -17868,7 +17872,7 @@ function ac_copy_rows(gobj, event, kw, src) {
17868
17872
  * From internal
17869
17873
  ************************************************************/
17870
17874
  function ac_paste_rows(gobj, event, kw, src) {
17871
- if (refuse_if_readonly(gobj, event)) return -1;
17875
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17872
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", {
17873
17877
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
17874
17878
  record: row
@@ -17881,7 +17885,7 @@ function ac_paste_rows(gobj, event, kw, src) {
17881
17885
  * fkeys, times and numbers). Route by the form's mode.
17882
17886
  ************************************************************/
17883
17887
  function ac_form_save_record(gobj, event, kw, src) {
17884
- if (refuse_if_readonly(gobj, event)) return -1;
17888
+ if (refuse_if_readonly$1(gobj, event)) return -1;
17885
17889
  let mode = gobj_read_str_attr(src, "form_mode");
17886
17890
  gobj_publish_event(gobj, mode === "create" ? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD", {
17887
17891
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
@@ -18289,6 +18293,7 @@ var attrs_table$3 = [
18289
18293
  SDATA(data_type_t.DTP_STRING, "treedb_name", 0, null, "Remote service treedb name"),
18290
18294
  SDATA(data_type_t.DTP_DICT, "descs", 0, null, "Descriptions of topics obtained"),
18291
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`)"),
18292
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)"),
18293
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)."),
18294
18299
  SDATA(data_type_t.DTP_DICT, "records", 0, "{}", "Data of topics"),
@@ -18339,6 +18344,7 @@ function mt_create$3(gobj) {
18339
18344
  if (!subscriber) subscriber = gobj_parent(gobj);
18340
18345
  gobj_subscribe_event(gobj, null, {}, subscriber);
18341
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");
18342
18348
  let canvas_id = clean_name(gobj_name(gobj)) + "-canvas";
18343
18349
  gobj_write_attr(gobj, "canvas_id", canvas_id);
18344
18350
  let $container_canvas = build_ui$3(gobj).querySelector(`#${priv.canvas_id}`);
@@ -18493,6 +18499,8 @@ function destroy_ui(gobj) {
18493
18499
  ************************************************************/
18494
18500
  function make_toolbar(gobj) {
18495
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");
18496
18504
  const $toolbar_header = yui_toolbar({}, [
18497
18505
  [
18498
18506
  "div",
@@ -18531,7 +18539,7 @@ function make_toolbar(gobj) {
18531
18539
  [[
18532
18540
  "select",
18533
18541
  { class: "GRAPH_MODE_SELECT" },
18534
- gobj_read_attr(gobj, "operation_modes").map((item) => [
18542
+ modes.map((item) => [
18535
18543
  "option",
18536
18544
  {},
18537
18545
  item
@@ -19083,6 +19091,19 @@ function refresh_data(gobj) {
19083
19091
  if (priv.gobj_nodes_tree) gobj_send_event(priv.gobj_nodes_tree, "EV_CLEAR_DATA", {}, gobj);
19084
19092
  request_treedb_descs(gobj);
19085
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
+ }
19086
19107
  /***************************
19087
19108
  * Actions
19088
19109
  ***************************/
@@ -19151,7 +19172,15 @@ function ac_mt_command_answer(gobj, event, kw, src) {
19151
19172
  case "update-node":
19152
19173
  case "delete-node":
19153
19174
  case "link-nodes":
19154
- case "unlink-nodes": break;
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;
19155
19184
  default: log_error(`${gobj_short_name(gobj)} Command unknown: ${command}`);
19156
19185
  }
19157
19186
  return 0;
@@ -19370,7 +19399,9 @@ function ac_edge_clicked(gobj, event, kw, src) {
19370
19399
  * Send to backend
19371
19400
  ********************************************/
19372
19401
  function ac_create_node(gobj, event, kw, src) {
19373
- let treedb_name = gobj.priv.treedb_name;
19402
+ let priv = gobj.priv;
19403
+ if (refuse_if_readonly(gobj, event)) return -1;
19404
+ let treedb_name = priv.treedb_name;
19374
19405
  let topic_name = kw.topic_name;
19375
19406
  let record = kw.record;
19376
19407
  return treedb_create_node(gobj, treedb_name, topic_name, record, kw.options || {});
@@ -19385,7 +19416,9 @@ function ac_create_node(gobj, event, kw, src) {
19385
19416
  * Send to backend
19386
19417
  ********************************************/
19387
19418
  function ac_update_node(gobj, event, kw, src) {
19388
- let treedb_name = gobj.priv.treedb_name;
19419
+ let priv = gobj.priv;
19420
+ if (refuse_if_readonly(gobj, event)) return -1;
19421
+ let treedb_name = priv.treedb_name;
19389
19422
  let topic_name = kw.topic_name;
19390
19423
  let record = kw.record;
19391
19424
  return treedb_update_node(gobj, treedb_name, topic_name, record, kw.options || {});
@@ -19400,7 +19433,9 @@ function ac_update_node(gobj, event, kw, src) {
19400
19433
  * Send to backend
19401
19434
  ********************************************/
19402
19435
  function ac_delete_node(gobj, event, kw, src) {
19403
- let treedb_name = gobj.priv.treedb_name;
19436
+ let priv = gobj.priv;
19437
+ if (refuse_if_readonly(gobj, event)) return -1;
19438
+ let treedb_name = priv.treedb_name;
19404
19439
  let topic_name = kw.topic_name;
19405
19440
  let record = kw.record;
19406
19441
  return treedb_delete_node(gobj, treedb_name, topic_name, record, kw.options || {});
@@ -19415,7 +19450,9 @@ function ac_delete_node(gobj, event, kw, src) {
19415
19450
  * Send to backend
19416
19451
  ********************************************/
19417
19452
  function ac_link_nodes(gobj, event, kw, src) {
19418
- let treedb_name = gobj.priv.treedb_name;
19453
+ let priv = gobj.priv;
19454
+ if (refuse_if_readonly(gobj, event)) return -1;
19455
+ let treedb_name = priv.treedb_name;
19419
19456
  let parent_ref = kw.parent_ref;
19420
19457
  let child_ref = kw.child_ref;
19421
19458
  return treedb_link_nodes(gobj, treedb_name, parent_ref, child_ref, kw.options || {});
@@ -19430,7 +19467,9 @@ function ac_link_nodes(gobj, event, kw, src) {
19430
19467
  * Send to backend
19431
19468
  ********************************************/
19432
19469
  function ac_unlink_nodes(gobj, event, kw, src) {
19433
- let treedb_name = gobj.priv.treedb_name;
19470
+ let priv = gobj.priv;
19471
+ if (refuse_if_readonly(gobj, event)) return -1;
19472
+ let treedb_name = priv.treedb_name;
19434
19473
  let parent_ref = kw.parent_ref;
19435
19474
  let child_ref = kw.child_ref;
19436
19475
  return treedb_unlink_nodes(gobj, treedb_name, parent_ref, child_ref, kw.options || {});
@@ -19470,6 +19509,10 @@ function ac_set_layout$1(gobj, event, kw, src) {
19470
19509
  function ac_set_operation_mode$1(gobj, event, kw, src) {
19471
19510
  let priv = gobj.priv;
19472
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
+ }
19473
19516
  gobj_write_str_attr(gobj, "operation_mode", operation_mode);
19474
19517
  gobj_save_persistent_attrs(gobj, "operation_mode");
19475
19518
  let $operation_mode_select = priv.$container.querySelector(".GRAPH_MODE_SELECT");
@@ -19691,6 +19734,7 @@ function create_gclass$3(gclass_name) {
19691
19734
  ["EV_SET_OPERATION_MODE", 0],
19692
19735
  ["EV_SET_FOCUS_TOPIC", 0],
19693
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],
19694
19738
  ["EV_SHOW", 0],
19695
19739
  ["EV_HIDE", 0],
19696
19740
  ["EV_TRANSPORT_STATE", 0]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "5.15.0",
3
+ "version": "5.16.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -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
- // Don't process here, process on subscribed events.
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],
@@ -2242,7 +2242,22 @@ function ac_load_node_updated(gobj, event, kw, src)
2242
2242
  let tabulator = gobj_read_attr(gobj, "tabulator");
2243
2243
  if(tabulator) {
2244
2244
  for(let record of data) {
2245
- tabulator.updateData([record]);
2245
+ /*
2246
+ * updateData() REJECTS the promise on a row it cannot find and
2247
+ * nobody awaits it, so the rejection surfaces as an unhandled
2248
+ * "Update Error - Unable to find row" that names neither the
2249
+ * gclass nor the topic. Ask first, like ac_node_deleted does.
2250
+ *
2251
+ * A table nobody has opened holds NO rows, and a node event for
2252
+ * it is not news: its rows are read when the topic is shown. A
2253
+ * row missing from a table that IS loaded is.
2254
+ */
2255
+ if(tabulator.getRow(record.id)) {
2256
+ tabulator.updateData([record]);
2257
+ } else if(tabulator.getDataCount() > 0) {
2258
+ log_error(`${gobj_short_name(gobj)}: updated node is not in ` +
2259
+ `the loaded table: ${record.id}`);
2260
+ }
2246
2261
  }
2247
2262
  }
2248
2263
 
@@ -1755,7 +1755,8 @@ function ac_mt_command_answer(gobj, event, kw, src)
1755
1755
  treedb_name: kw_get_str(gobj, kw_command, "treedb_name", "", 0),
1756
1756
  topic_name: kw_get_str(gobj, kw_command, "topic_name", "", 0),
1757
1757
  record: kw_get_dict(gobj, kw_command, "record", {}, 0),
1758
- created: (command === "create-node")
1758
+ created: (command === "create-node"),
1759
+ command: command
1759
1760
  });
1760
1761
  break;
1761
1762