@yuneta/gobj-ui 7.7.1 → 7.8.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.
@@ -15609,7 +15609,13 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
15609
15609
  }
15610
15610
  if (result < 0) {
15611
15611
  if (command === "descs") show_load_error$1(gobj, (0, i18next.t)(comment));
15612
- else yui_shell_show_error(yui_shell_of(gobj), comment, { t: i18next.t });
15612
+ else {
15613
+ yui_shell_show_error(yui_shell_of(gobj), comment, { t: i18next.t });
15614
+ if (command === "update-node" || command === "create-node") {
15615
+ let failed_topic = (0, _yuneta_gobj_js.kw_get_str)(gobj, kw_command, "topic_name", "", 0);
15616
+ if (failed_topic) treedb_nodes$1(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name"), failed_topic, { list_dict: true });
15617
+ }
15618
+ }
15613
15619
  return 0;
15614
15620
  }
15615
15621
  switch (command) {
@@ -15849,6 +15855,35 @@ function ac_create_record(gobj, event, kw, src) {
15849
15855
  });
15850
15856
  }
15851
15857
  /********************************************
15858
+ * Event from formtable: ONE field of one record, edited in place.
15859
+ *
15860
+ * Written as a PARTIAL update and, deliberately, with NO `autolink`.
15861
+ * `treedb_update_node()` merges (`json_object_update`), so the fields
15862
+ * it does not carry are left alone; and `autolink` is what wipes a
15863
+ * node's links to rebuild them from the fkeys the record carries — on
15864
+ * a partial record that reads as "no parents" and DETACHES the node,
15865
+ * answering success. A cell edit changes a scalar and must not be able
15866
+ * to touch a link at all.
15867
+ *
15868
+ * This is why it is its own event and not EV_UPDATE_RECORD: that one
15869
+ * DOES send autolink, and may, because the form hands it the whole
15870
+ * record with its fkeys in it.
15871
+ ********************************************/
15872
+ function ac_update_field(gobj, event, kw, src) {
15873
+ if (refuse_if_readonly$3(gobj, event)) return -1;
15874
+ let topic_name = kw.topic_name || (0, _yuneta_gobj_js.gobj_read_attr)(src, "topic_name");
15875
+ let desc = ((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "descs") || {})[topic_name];
15876
+ let pkey = desc && desc.pkey || "id";
15877
+ if (kw.id === void 0 || kw.id === null || !kw.field) {
15878
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: a field write with no id or no field`);
15879
+ return -1;
15880
+ }
15881
+ let record = {};
15882
+ record[pkey] = kw.id;
15883
+ record[kw.field] = kw.value;
15884
+ return treedb_update_node$1(gobj, (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "treedb_name"), topic_name, record, { list_dict: true });
15885
+ }
15886
+ /********************************************
15852
15887
  * Event from formtable
15853
15888
  * kw: {
15854
15889
  * topic_name,
@@ -15970,6 +16005,11 @@ function create_gclass$6(gclass_name) {
15970
16005
  ac_update_record,
15971
16006
  null
15972
16007
  ],
16008
+ [
16009
+ "EV_UPDATE_FIELD",
16010
+ ac_update_field,
16011
+ null
16012
+ ],
15973
16013
  [
15974
16014
  "EV_DELETE_RECORD",
15975
16015
  ac_delete_record,
@@ -16043,6 +16083,7 @@ function create_gclass$6(gclass_name) {
16043
16083
  ["EV_TREEDB_NODE_DELETED", _yuneta_gobj_js.event_flag_t.EVF_PUBLIC_EVENT],
16044
16084
  ["EV_CREATE_RECORD", 0],
16045
16085
  ["EV_UPDATE_RECORD", 0],
16086
+ ["EV_UPDATE_FIELD", 0],
16046
16087
  ["EV_DELETE_RECORD", 0],
16047
16088
  ["EV_RECORD_WRITTEN", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT | _yuneta_gobj_js.event_flag_t.EVF_NO_WARN_SUBS],
16048
16089
  ["EV_REFRESH_TOPIC", 0],
@@ -16256,6 +16297,7 @@ var attrs_table$5 = [
16256
16297
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button toolbar COLUMNS (choose which columns the table shows)"),
16257
16298
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
16258
16299
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_header_filters", 0, true, "Per-column filter box in the table header, on the columns a text/number match means something (not hooks, fkeys or json)"),
16300
+ (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_inline_edit", 0, true, "Edit a writable scalar cell in place while in edition mode (the record form keeps the rest)"),
16259
16301
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
16260
16302
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16261
16303
  (0, _yuneta_gobj_js.SDATA)(_yuneta_gobj_js.data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
@@ -16765,6 +16807,7 @@ function create_tabulator(gobj) {
16765
16807
  let table_id = (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "table_id");
16766
16808
  let desc = (0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc");
16767
16809
  let with_header_filters = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_header_filters");
16810
+ let with_inline_edit = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_inline_edit");
16768
16811
  let columns = [];
16769
16812
  let with_checkbox = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_checkbox");
16770
16813
  let with_radio = (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "with_radio");
@@ -16808,6 +16851,42 @@ function create_tabulator(gobj) {
16808
16851
  col_id: cell.getField()
16809
16852
  }, gobj);
16810
16853
  }
16854
+ const INLINE_EDITABLE = [
16855
+ "string",
16856
+ "integer",
16857
+ "real",
16858
+ "email",
16859
+ "url",
16860
+ "tel",
16861
+ "id",
16862
+ "hex",
16863
+ "currency",
16864
+ "percent",
16865
+ "uuid"
16866
+ ];
16867
+ function inline_editor_for(colDef, col, field_desc) {
16868
+ if (!field_desc.is_writable) return;
16869
+ if (col.id === (desc.pkey || "id")) return;
16870
+ switch (field_desc.type) {
16871
+ case "boolean":
16872
+ colDef.editor = "tickCross";
16873
+ break;
16874
+ case "enum":
16875
+ colDef.editor = "list";
16876
+ colDef.editorParams = { values: field_desc.enum_list || [] };
16877
+ break;
16878
+ case "integer":
16879
+ case "real":
16880
+ colDef.editor = "number";
16881
+ break;
16882
+ default:
16883
+ if (!INLINE_EDITABLE.includes(field_desc.type)) return;
16884
+ colDef.editor = "input";
16885
+ }
16886
+ colDef.editable = function() {
16887
+ return (0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "editable") && !(0, _yuneta_gobj_js.gobj_read_bool_attr)(gobj, "readonly");
16888
+ };
16889
+ }
16811
16890
  const FILTERABLE = [
16812
16891
  "string",
16813
16892
  "integer",
@@ -16930,6 +17009,7 @@ function create_tabulator(gobj) {
16930
17009
  formatter: colFormatter
16931
17010
  };
16932
17011
  if (with_header_filters) apply_header_filter(colDef, field_desc);
17012
+ if (with_inline_edit) inline_editor_for(colDef, col, field_desc);
16933
17013
  if (vertAlign) colDef.vertAlign = vertAlign;
16934
17014
  if (formatterParams) colDef.formatterParams = formatterParams;
16935
17015
  if (cellClick) colDef.cellClick = cellClick;
@@ -17016,6 +17096,21 @@ function create_tabulator(gobj) {
17016
17096
  });
17017
17097
  tabulator.on("dataProcessed", update_rowcount);
17018
17098
  tabulator.on("dataChanged", update_rowcount);
17099
+ tabulator.on("cellEdited", function(cell) {
17100
+ let pkey = ((0, _yuneta_gobj_js.gobj_read_attr)(gobj, "desc") || {}).pkey || "id";
17101
+ let data = cell.getRow().getData();
17102
+ let id = data ? data[pkey] : void 0;
17103
+ if (id === void 0 || id === null) {
17104
+ (0, _yuneta_gobj_js.log_error)(`${(0, _yuneta_gobj_js.gobj_short_name)(gobj)}: edited a row with no '${pkey}'`);
17105
+ cell.restoreOldValue();
17106
+ return;
17107
+ }
17108
+ (0, _yuneta_gobj_js.gobj_send_event)(gobj, "EV_CELL_EDITED", {
17109
+ id,
17110
+ field: cell.getField(),
17111
+ value: cell.getValue()
17112
+ }, gobj);
17113
+ });
17019
17114
  tabulator.on("dataFiltered", function(filters, rows) {
17020
17115
  update_rowcount(Array.isArray(rows) ? rows.length : void 0);
17021
17116
  });
@@ -18136,6 +18231,20 @@ function ac_show_cell_json(gobj, event, kw, src) {
18136
18231
  return 0;
18137
18232
  }
18138
18233
  /************************************************************
18234
+ * One cell of one record changed. Publish it UP as a field write;
18235
+ * the host owns the treedb command and its options.
18236
+ ************************************************************/
18237
+ function ac_cell_edited(gobj, event, kw, src) {
18238
+ if (refuse_if_readonly$2(gobj, event)) return -1;
18239
+ (0, _yuneta_gobj_js.gobj_publish_event)(gobj, "EV_UPDATE_FIELD", {
18240
+ topic_name: (0, _yuneta_gobj_js.gobj_read_str_attr)(gobj, "topic_name"),
18241
+ id: kw.id,
18242
+ field: kw.field,
18243
+ value: kw.value
18244
+ });
18245
+ return 0;
18246
+ }
18247
+ /************************************************************
18139
18248
  * Show the schema (desc) of this topic
18140
18249
  ************************************************************/
18141
18250
  function ac_show_schema(gobj, event, kw, src) {
@@ -18385,6 +18494,11 @@ function create_gclass$5(gclass_name) {
18385
18494
  ac_show_schema,
18386
18495
  null
18387
18496
  ],
18497
+ [
18498
+ "EV_CELL_EDITED",
18499
+ ac_cell_edited,
18500
+ null
18501
+ ],
18388
18502
  [
18389
18503
  "EV_SEARCH",
18390
18504
  ac_search,
@@ -18435,6 +18549,8 @@ function create_gclass$5(gclass_name) {
18435
18549
  ["EV_CHANGE_LOCALE", 0],
18436
18550
  ["EV_REFRESH", 0],
18437
18551
  ["EV_SHOW_SCHEMA", 0],
18552
+ ["EV_CELL_EDITED", 0],
18553
+ ["EV_UPDATE_FIELD", _yuneta_gobj_js.event_flag_t.EVF_OUTPUT_EVENT],
18438
18554
  ["EV_SEARCH", 0],
18439
18555
  ["EV_OPEN_COLUMNS", 0],
18440
18556
  ["EV_TOGGLE_COLUMN", 0],
@@ -15604,7 +15604,13 @@ function ac_mt_command_answer$2(gobj, event, kw, src) {
15604
15604
  }
15605
15605
  if (result < 0) {
15606
15606
  if (command === "descs") show_load_error$1(gobj, t(comment));
15607
- else yui_shell_show_error(yui_shell_of(gobj), comment, { t });
15607
+ else {
15608
+ yui_shell_show_error(yui_shell_of(gobj), comment, { t });
15609
+ if (command === "update-node" || command === "create-node") {
15610
+ let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
15611
+ if (failed_topic) treedb_nodes$1(gobj, gobj_read_str_attr(gobj, "treedb_name"), failed_topic, { list_dict: true });
15612
+ }
15613
+ }
15608
15614
  return 0;
15609
15615
  }
15610
15616
  switch (command) {
@@ -15844,6 +15850,35 @@ function ac_create_record(gobj, event, kw, src) {
15844
15850
  });
15845
15851
  }
15846
15852
  /********************************************
15853
+ * Event from formtable: ONE field of one record, edited in place.
15854
+ *
15855
+ * Written as a PARTIAL update and, deliberately, with NO `autolink`.
15856
+ * `treedb_update_node()` merges (`json_object_update`), so the fields
15857
+ * it does not carry are left alone; and `autolink` is what wipes a
15858
+ * node's links to rebuild them from the fkeys the record carries — on
15859
+ * a partial record that reads as "no parents" and DETACHES the node,
15860
+ * answering success. A cell edit changes a scalar and must not be able
15861
+ * to touch a link at all.
15862
+ *
15863
+ * This is why it is its own event and not EV_UPDATE_RECORD: that one
15864
+ * DOES send autolink, and may, because the form hands it the whole
15865
+ * record with its fkeys in it.
15866
+ ********************************************/
15867
+ function ac_update_field(gobj, event, kw, src) {
15868
+ if (refuse_if_readonly$3(gobj, event)) return -1;
15869
+ let topic_name = kw.topic_name || gobj_read_attr(src, "topic_name");
15870
+ let desc = (gobj_read_attr(gobj, "descs") || {})[topic_name];
15871
+ let pkey = desc && desc.pkey || "id";
15872
+ if (kw.id === void 0 || kw.id === null || !kw.field) {
15873
+ log_error(`${gobj_short_name(gobj)}: a field write with no id or no field`);
15874
+ return -1;
15875
+ }
15876
+ let record = {};
15877
+ record[pkey] = kw.id;
15878
+ record[kw.field] = kw.value;
15879
+ return treedb_update_node$1(gobj, gobj_read_str_attr(gobj, "treedb_name"), topic_name, record, { list_dict: true });
15880
+ }
15881
+ /********************************************
15847
15882
  * Event from formtable
15848
15883
  * kw: {
15849
15884
  * topic_name,
@@ -15965,6 +16000,11 @@ function create_gclass$6(gclass_name) {
15965
16000
  ac_update_record,
15966
16001
  null
15967
16002
  ],
16003
+ [
16004
+ "EV_UPDATE_FIELD",
16005
+ ac_update_field,
16006
+ null
16007
+ ],
15968
16008
  [
15969
16009
  "EV_DELETE_RECORD",
15970
16010
  ac_delete_record,
@@ -16038,6 +16078,7 @@ function create_gclass$6(gclass_name) {
16038
16078
  ["EV_TREEDB_NODE_DELETED", event_flag_t.EVF_PUBLIC_EVENT],
16039
16079
  ["EV_CREATE_RECORD", 0],
16040
16080
  ["EV_UPDATE_RECORD", 0],
16081
+ ["EV_UPDATE_FIELD", 0],
16041
16082
  ["EV_DELETE_RECORD", 0],
16042
16083
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT | event_flag_t.EVF_NO_WARN_SUBS],
16043
16084
  ["EV_REFRESH_TOPIC", 0],
@@ -16251,6 +16292,7 @@ var attrs_table$5 = [
16251
16292
  SDATA(data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button toolbar COLUMNS (choose which columns the table shows)"),
16252
16293
  SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
16253
16294
  SDATA(data_type_t.DTP_BOOLEAN, "with_header_filters", 0, true, "Per-column filter box in the table header, on the columns a text/number match means something (not hooks, fkeys or json)"),
16295
+ SDATA(data_type_t.DTP_BOOLEAN, "with_inline_edit", 0, true, "Edit a writable scalar cell in place while in edition mode (the record form keeps the rest)"),
16254
16296
  SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
16255
16297
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
16256
16298
  SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, true, "Auxiliary first column to select rows"),
@@ -16760,6 +16802,7 @@ function create_tabulator(gobj) {
16760
16802
  let table_id = gobj_read_str_attr(gobj, "table_id");
16761
16803
  let desc = gobj_read_attr(gobj, "desc");
16762
16804
  let with_header_filters = gobj_read_bool_attr(gobj, "with_header_filters");
16805
+ let with_inline_edit = gobj_read_bool_attr(gobj, "with_inline_edit");
16763
16806
  let columns = [];
16764
16807
  let with_checkbox = gobj_read_bool_attr(gobj, "with_checkbox");
16765
16808
  let with_radio = gobj_read_bool_attr(gobj, "with_radio");
@@ -16803,6 +16846,42 @@ function create_tabulator(gobj) {
16803
16846
  col_id: cell.getField()
16804
16847
  }, gobj);
16805
16848
  }
16849
+ const INLINE_EDITABLE = [
16850
+ "string",
16851
+ "integer",
16852
+ "real",
16853
+ "email",
16854
+ "url",
16855
+ "tel",
16856
+ "id",
16857
+ "hex",
16858
+ "currency",
16859
+ "percent",
16860
+ "uuid"
16861
+ ];
16862
+ function inline_editor_for(colDef, col, field_desc) {
16863
+ if (!field_desc.is_writable) return;
16864
+ if (col.id === (desc.pkey || "id")) return;
16865
+ switch (field_desc.type) {
16866
+ case "boolean":
16867
+ colDef.editor = "tickCross";
16868
+ break;
16869
+ case "enum":
16870
+ colDef.editor = "list";
16871
+ colDef.editorParams = { values: field_desc.enum_list || [] };
16872
+ break;
16873
+ case "integer":
16874
+ case "real":
16875
+ colDef.editor = "number";
16876
+ break;
16877
+ default:
16878
+ if (!INLINE_EDITABLE.includes(field_desc.type)) return;
16879
+ colDef.editor = "input";
16880
+ }
16881
+ colDef.editable = function() {
16882
+ return gobj_read_bool_attr(gobj, "editable") && !gobj_read_bool_attr(gobj, "readonly");
16883
+ };
16884
+ }
16806
16885
  const FILTERABLE = [
16807
16886
  "string",
16808
16887
  "integer",
@@ -16925,6 +17004,7 @@ function create_tabulator(gobj) {
16925
17004
  formatter: colFormatter
16926
17005
  };
16927
17006
  if (with_header_filters) apply_header_filter(colDef, field_desc);
17007
+ if (with_inline_edit) inline_editor_for(colDef, col, field_desc);
16928
17008
  if (vertAlign) colDef.vertAlign = vertAlign;
16929
17009
  if (formatterParams) colDef.formatterParams = formatterParams;
16930
17010
  if (cellClick) colDef.cellClick = cellClick;
@@ -17011,6 +17091,21 @@ function create_tabulator(gobj) {
17011
17091
  });
17012
17092
  tabulator.on("dataProcessed", update_rowcount);
17013
17093
  tabulator.on("dataChanged", update_rowcount);
17094
+ tabulator.on("cellEdited", function(cell) {
17095
+ let pkey = (gobj_read_attr(gobj, "desc") || {}).pkey || "id";
17096
+ let data = cell.getRow().getData();
17097
+ let id = data ? data[pkey] : void 0;
17098
+ if (id === void 0 || id === null) {
17099
+ log_error(`${gobj_short_name(gobj)}: edited a row with no '${pkey}'`);
17100
+ cell.restoreOldValue();
17101
+ return;
17102
+ }
17103
+ gobj_send_event(gobj, "EV_CELL_EDITED", {
17104
+ id,
17105
+ field: cell.getField(),
17106
+ value: cell.getValue()
17107
+ }, gobj);
17108
+ });
17014
17109
  tabulator.on("dataFiltered", function(filters, rows) {
17015
17110
  update_rowcount(Array.isArray(rows) ? rows.length : void 0);
17016
17111
  });
@@ -18131,6 +18226,20 @@ function ac_show_cell_json(gobj, event, kw, src) {
18131
18226
  return 0;
18132
18227
  }
18133
18228
  /************************************************************
18229
+ * One cell of one record changed. Publish it UP as a field write;
18230
+ * the host owns the treedb command and its options.
18231
+ ************************************************************/
18232
+ function ac_cell_edited(gobj, event, kw, src) {
18233
+ if (refuse_if_readonly$2(gobj, event)) return -1;
18234
+ gobj_publish_event(gobj, "EV_UPDATE_FIELD", {
18235
+ topic_name: gobj_read_str_attr(gobj, "topic_name"),
18236
+ id: kw.id,
18237
+ field: kw.field,
18238
+ value: kw.value
18239
+ });
18240
+ return 0;
18241
+ }
18242
+ /************************************************************
18134
18243
  * Show the schema (desc) of this topic
18135
18244
  ************************************************************/
18136
18245
  function ac_show_schema(gobj, event, kw, src) {
@@ -18380,6 +18489,11 @@ function create_gclass$5(gclass_name) {
18380
18489
  ac_show_schema,
18381
18490
  null
18382
18491
  ],
18492
+ [
18493
+ "EV_CELL_EDITED",
18494
+ ac_cell_edited,
18495
+ null
18496
+ ],
18383
18497
  [
18384
18498
  "EV_SEARCH",
18385
18499
  ac_search,
@@ -18430,6 +18544,8 @@ function create_gclass$5(gclass_name) {
18430
18544
  ["EV_CHANGE_LOCALE", 0],
18431
18545
  ["EV_REFRESH", 0],
18432
18546
  ["EV_SHOW_SCHEMA", 0],
18547
+ ["EV_CELL_EDITED", 0],
18548
+ ["EV_UPDATE_FIELD", event_flag_t.EVF_OUTPUT_EVENT],
18433
18549
  ["EV_SEARCH", 0],
18434
18550
  ["EV_OPEN_COLUMNS", 0],
18435
18551
  ["EV_TOGGLE_COLUMN", 0],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.7.1",
3
+ "version": "7.8.0",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -111,6 +111,7 @@ SDATA(data_type_t.DTP_BOOLEAN, "with_schema_button", 0, true, "Button
111
111
  SDATA(data_type_t.DTP_BOOLEAN, "with_columns_button", 0, true, "Button toolbar COLUMNS (choose which columns the table shows)"),
112
112
  SDATA(data_type_t.DTP_BOOLEAN, "with_export_button", 0, true, "Button toolbar EXPORT (download as CSV what the table holds)"),
113
113
  SDATA(data_type_t.DTP_BOOLEAN, "with_header_filters", 0, true, "Per-column filter box in the table header, on the columns a text/number match means something (not hooks, fkeys or json)"),
114
+ SDATA(data_type_t.DTP_BOOLEAN, "with_inline_edit", 0, true, "Edit a writable scalar cell in place while in edition mode (the record form keeps the rest)"),
114
115
  SDATA(data_type_t.DTP_BOOLEAN, "with_in_row_edit_icons", 0, true, "Add a last column with internal EDIT/DELETE icon"),
115
116
 
116
117
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Edit state"),
@@ -730,6 +731,7 @@ function create_tabulator(gobj)
730
731
  let table_id = gobj_read_str_attr(gobj, "table_id");
731
732
  let desc = gobj_read_attr(gobj, "desc");
732
733
  let with_header_filters = gobj_read_bool_attr(gobj, "with_header_filters");
734
+ let with_inline_edit = gobj_read_bool_attr(gobj, "with_inline_edit");
733
735
 
734
736
  let columns = [];
735
737
 
@@ -819,6 +821,62 @@ function create_tabulator(gobj)
819
821
  * value is stringified first because a fkey can arrive as a ref string,
820
822
  * a list of them or a dict.
821
823
  */
824
+ /* A cell you can type into, and the ones you cannot.
825
+ *
826
+ * Editing in place is for a SCALAR the table can round-trip. A hook
827
+ * holds children and an fkey IS a link — both are edited by linking,
828
+ * not by typing — a dict or a list is a document (the form has an
829
+ * editor for it), and a date cell shows a formatted string over an
830
+ * epoch, so typing into it would write the string. Those stay with
831
+ * the form, which is one click away on the same row.
832
+ *
833
+ * The schema decides the rest: only a column flagged `writable` is
834
+ * offered, and never the pkey — renaming what a record is KEYED by
835
+ * is not a field edit.
836
+ */
837
+ const INLINE_EDITABLE = [
838
+ "string", "integer", "real",
839
+ "email", "url", "tel", "id", "hex", "currency", "percent", "uuid"
840
+ ];
841
+
842
+ function inline_editor_for(colDef, col, field_desc)
843
+ {
844
+ if(!field_desc.is_writable) {
845
+ return;
846
+ }
847
+ if(col.id === (desc.pkey || "id")) {
848
+ return;
849
+ }
850
+
851
+ switch(field_desc.type) {
852
+ case "boolean":
853
+ colDef.editor = "tickCross";
854
+ break;
855
+ case "enum":
856
+ colDef.editor = "list";
857
+ colDef.editorParams = {values: field_desc.enum_list || []};
858
+ break;
859
+ case "integer":
860
+ case "real":
861
+ colDef.editor = "number";
862
+ break;
863
+ default:
864
+ if(!INLINE_EDITABLE.includes(field_desc.type)) {
865
+ return;
866
+ }
867
+ colDef.editor = "input";
868
+ break;
869
+ }
870
+
871
+ /* A FUNCTION, not a flag: edition mode is toggled on a table that
872
+ * is already built (ac_edition_mode only shows and hides columns),
873
+ * so the answer has to be asked for at the moment of the click. */
874
+ colDef.editable = function() {
875
+ return gobj_read_bool_attr(gobj, "editable") &&
876
+ !gobj_read_bool_attr(gobj, "readonly");
877
+ };
878
+ }
879
+
822
880
  const FILTERABLE = [
823
881
  "string", "integer", "real", "rowid", "uuid", "qualified",
824
882
  "email", "url", "tel", "id", "hex", "currency", "percent"
@@ -974,6 +1032,9 @@ function create_tabulator(gobj)
974
1032
  if(with_header_filters) {
975
1033
  apply_header_filter(colDef, field_desc);
976
1034
  }
1035
+ if(with_inline_edit) {
1036
+ inline_editor_for(colDef, col, field_desc);
1037
+ }
977
1038
  if(vertAlign) {
978
1039
  colDef.vertAlign = vertAlign;
979
1040
  }
@@ -1139,6 +1200,33 @@ function create_tabulator(gobj)
1139
1200
  * box; a filter per column made it a claim you read on every keystroke. */
1140
1201
  tabulator.on("dataProcessed", update_rowcount);
1141
1202
  tabulator.on("dataChanged", update_rowcount);
1203
+ /* A cell edited in place. Announced as ONE FIELD of one record — not
1204
+ * as the row — and that is the whole safety of it: the host writes it
1205
+ * with a partial update and no `autolink`, so nothing but that field
1206
+ * moves. `autolink` wipes a node's links and rebuilds them from the
1207
+ * fkeys the record carries, so sending a row that the table shaped for
1208
+ * DISPLAY would unlink the node and answer success. */
1209
+ tabulator.on("cellEdited", function(cell) {
1210
+ let pkey = (gobj_read_attr(gobj, "desc") || {}).pkey || "id";
1211
+ let data = cell.getRow().getData();
1212
+ let id = data ? data[pkey] : undefined;
1213
+ if(id === undefined || id === null) {
1214
+ log_error(`${gobj_short_name(gobj)}: edited a row with no '${pkey}'`);
1215
+ cell.restoreOldValue();
1216
+ return;
1217
+ }
1218
+ gobj_send_event(
1219
+ gobj,
1220
+ "EV_CELL_EDITED",
1221
+ {
1222
+ id: id,
1223
+ field: cell.getField(),
1224
+ value: cell.getValue()
1225
+ },
1226
+ gobj
1227
+ );
1228
+ });
1229
+
1142
1230
  tabulator.on("dataFiltered", function(filters, rows) {
1143
1231
  update_rowcount(Array.isArray(rows)? rows.length : undefined);
1144
1232
  });
@@ -2845,6 +2933,28 @@ function ac_show_cell_json(gobj, event, kw, src)
2845
2933
  return 0;
2846
2934
  }
2847
2935
 
2936
+ /************************************************************
2937
+ * One cell of one record changed. Publish it UP as a field write;
2938
+ * the host owns the treedb command and its options.
2939
+ ************************************************************/
2940
+ function ac_cell_edited(gobj, event, kw, src)
2941
+ {
2942
+ if(refuse_if_readonly(gobj, event)) {
2943
+ return -1; /* Error already logged */
2944
+ }
2945
+ gobj_publish_event(
2946
+ gobj,
2947
+ "EV_UPDATE_FIELD",
2948
+ {
2949
+ topic_name: gobj_read_str_attr(gobj, "topic_name"),
2950
+ id: kw.id,
2951
+ field: kw.field,
2952
+ value: kw.value
2953
+ }
2954
+ );
2955
+ return 0;
2956
+ }
2957
+
2848
2958
  /************************************************************
2849
2959
  * Show the schema (desc) of this topic
2850
2960
  ************************************************************/
@@ -3076,6 +3186,7 @@ function create_gclass(gclass_name)
3076
3186
  ["EV_CHANGE_LOCALE", ac_change_locale, null],
3077
3187
  ["EV_REFRESH", ac_refresh, null],
3078
3188
  ["EV_SHOW_SCHEMA", ac_show_schema, null],
3189
+ ["EV_CELL_EDITED", ac_cell_edited, null],
3079
3190
  ["EV_SEARCH", ac_search, null],
3080
3191
  ["EV_OPEN_COLUMNS", ac_open_columns, null],
3081
3192
  ["EV_TOGGLE_COLUMN", ac_toggle_column, null],
@@ -3108,6 +3219,8 @@ function create_gclass(gclass_name)
3108
3219
  ["EV_CHANGE_LOCALE", 0],
3109
3220
  ["EV_REFRESH", 0],
3110
3221
  ["EV_SHOW_SCHEMA", 0],
3222
+ ["EV_CELL_EDITED", 0],
3223
+ ["EV_UPDATE_FIELD", event_flag_t.EVF_OUTPUT_EVENT],
3111
3224
  ["EV_SEARCH", 0],
3112
3225
  ["EV_OPEN_COLUMNS", 0],
3113
3226
  ["EV_TOGGLE_COLUMN", 0],
@@ -1740,6 +1740,18 @@ function ac_mt_command_answer(gobj, event, kw, src)
1740
1740
  show_load_error(gobj, t(comment));
1741
1741
  } else {
1742
1742
  yui_shell_show_error(yui_shell_of(gobj), comment, {t: t});
1743
+ /* A refused write leaves the TABLE showing what the operator
1744
+ * typed and the store holding what it had. That is tolerable
1745
+ * for a form (it stays open on the values it failed with) and
1746
+ * is not for a cell edited in place, which looks saved. Put the
1747
+ * topic back to what the treedb actually has. */
1748
+ if(command === "update-node" || command === "create-node") {
1749
+ let failed_topic = kw_get_str(gobj, kw_command, "topic_name", "", 0);
1750
+ if(failed_topic) {
1751
+ treedb_nodes(gobj, gobj_read_str_attr(gobj, "treedb_name"),
1752
+ failed_topic, {list_dict: true});
1753
+ }
1754
+ }
1743
1755
  }
1744
1756
  return 0;
1745
1757
  }
@@ -2126,6 +2138,49 @@ function ac_create_record(gobj, event, kw, src)
2126
2138
  );
2127
2139
  }
2128
2140
 
2141
+ /********************************************
2142
+ * Event from formtable: ONE field of one record, edited in place.
2143
+ *
2144
+ * Written as a PARTIAL update and, deliberately, with NO `autolink`.
2145
+ * `treedb_update_node()` merges (`json_object_update`), so the fields
2146
+ * it does not carry are left alone; and `autolink` is what wipes a
2147
+ * node's links to rebuild them from the fkeys the record carries — on
2148
+ * a partial record that reads as "no parents" and DETACHES the node,
2149
+ * answering success. A cell edit changes a scalar and must not be able
2150
+ * to touch a link at all.
2151
+ *
2152
+ * This is why it is its own event and not EV_UPDATE_RECORD: that one
2153
+ * DOES send autolink, and may, because the form hands it the whole
2154
+ * record with its fkeys in it.
2155
+ ********************************************/
2156
+ function ac_update_field(gobj, event, kw, src)
2157
+ {
2158
+ if(refuse_if_readonly(gobj, event)) {
2159
+ return -1; /* Error already logged */
2160
+ }
2161
+ let topic_name = kw.topic_name || gobj_read_attr(src, "topic_name");
2162
+ let descs = gobj_read_attr(gobj, "descs") || {};
2163
+ let desc = descs[topic_name];
2164
+ let pkey = (desc && desc.pkey) || "id";
2165
+
2166
+ if(kw.id === undefined || kw.id === null || !kw.field) {
2167
+ log_error(`${gobj_short_name(gobj)}: a field write with no id or no field`);
2168
+ return -1;
2169
+ }
2170
+
2171
+ let record = {};
2172
+ record[pkey] = kw.id;
2173
+ record[kw.field] = kw.value;
2174
+
2175
+ return treedb_update_node(
2176
+ gobj,
2177
+ gobj_read_str_attr(gobj, "treedb_name"),
2178
+ topic_name,
2179
+ record,
2180
+ {list_dict: true}
2181
+ );
2182
+ }
2183
+
2129
2184
  /********************************************
2130
2185
  * Event from formtable
2131
2186
  * kw: {
@@ -2292,6 +2347,7 @@ function create_gclass(gclass_name)
2292
2347
  ["EV_TREEDB_NODE_DELETED", ac_treedb_node_deleted, null],
2293
2348
  ["EV_CREATE_RECORD", ac_create_record, null],
2294
2349
  ["EV_UPDATE_RECORD", ac_update_record, null],
2350
+ ["EV_UPDATE_FIELD", ac_update_field, null],
2295
2351
  ["EV_DELETE_RECORD", ac_delete_record, null],
2296
2352
  ["EV_REFRESH_TOPIC", ac_refresh_topic, null],
2297
2353
  ["EV_OPEN_JSON", ac_open_json, null],
@@ -2318,6 +2374,7 @@ function create_gclass(gclass_name)
2318
2374
  ["EV_TREEDB_NODE_DELETED", event_flag_t.EVF_PUBLIC_EVENT],
2319
2375
  ["EV_CREATE_RECORD", 0],
2320
2376
  ["EV_UPDATE_RECORD", 0],
2377
+ ["EV_UPDATE_FIELD", 0],
2321
2378
  ["EV_DELETE_RECORD", 0],
2322
2379
  ["EV_RECORD_WRITTEN", event_flag_t.EVF_OUTPUT_EVENT|
2323
2380
  event_flag_t.EVF_NO_WARN_SUBS],