@yuneta/gobj-ui 2.2.4 → 2.3.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.
@@ -27,7 +27,6 @@ import {
27
27
  is_string,
28
28
  is_object,
29
29
  str_in_list,
30
- createOneHtml,
31
30
  getPositionRelativeToBody,
32
31
  treedb_get_field_desc,
33
32
  treedb_decoder_fkey,
@@ -37,11 +36,15 @@ import {
37
36
  treedb_hook_data_size,
38
37
  parseBoolean,
39
38
  json_size,
40
- list2options,
41
39
  trace_msg,
42
40
  log_warning,
43
- kw_get_local_storage_value,
44
41
  kwid_get_ids,
42
+ gobj_create_pure_child,
43
+ gobj_start,
44
+ gobj_stop,
45
+ gobj_destroy,
46
+ gobj_is_running,
47
+ gclass_find_by_name,
45
48
  gobj_write_attr,
46
49
  gobj_read_bool_attr,
47
50
  gobj_read_str_attr,
@@ -58,13 +61,9 @@ import {
58
61
  get_ok,
59
62
  } from "./c_yui_main.js";
60
63
 
61
- import {t} from "i18next";
62
-
63
- import { JSONEditor } from 'vanilla-jsoneditor';
64
- import "vanilla-jsoneditor/themes/jse-theme-dark.css";
64
+ import {register_c_yui_form} from "./c_yui_form.js";
65
65
 
66
- import "tom-select/dist/css/tom-select.css"; // Import Tom-Select CSS
67
- import TomSelect from "tom-select"; // Import Tom-Select JS
66
+ import {t} from "i18next";
68
67
 
69
68
  import "./c_yui_treedb_topic_with_form.css";
70
69
 
@@ -121,7 +120,6 @@ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "HTML contai
121
120
  SDATA(data_type_t.DTP_POINTER, "tabulator", 0, null, "Tabulator instance"),
122
121
  SDATA(data_type_t.DTP_STRING, "table_id", 0, null, "Table div ID"),
123
122
  SDATA(data_type_t.DTP_STRING, "toolbar_id", 0, null, "Toolbar ID"),
124
- SDATA(data_type_t.DTP_STRING, "form_id", 0, null, "Edit form ID"),
125
123
  SDATA(data_type_t.DTP_STRING, "popup_id", 0, null, "Edit form popup ID"),
126
124
 
127
125
  SDATA_END()
@@ -132,6 +130,8 @@ let PRIVATE_DATA = {
132
130
  treedb_name: "",
133
131
  topic_name: "",
134
132
  tabulator: null,
133
+ form: null, // hosted C_YUI_FORM child (while dialog open)
134
+ $popup: null, // dialog DOM element
135
135
  };
136
136
 
137
137
  let __gclass__ = null;
@@ -163,7 +163,6 @@ function mt_create(gobj)
163
163
  let name = clean_name(gobj_name(gobj));
164
164
  gobj_write_attr(gobj, "table_id", "table" + name);
165
165
  gobj_write_attr(gobj, "toolbar_id", "toolbar" + name);
166
- gobj_write_attr(gobj, "form_id", "form" + name);
167
166
  gobj_write_attr(gobj, "popup_id", "popup" + name);
168
167
 
169
168
  build_ui(gobj);
@@ -191,6 +190,7 @@ function mt_start(gobj)
191
190
  ***************************************************************/
192
191
  function mt_stop(gobj)
193
192
  {
193
+ close_form_dialog(gobj);
194
194
  table__destroy(gobj);
195
195
  }
196
196
 
@@ -579,6 +579,29 @@ function col_label(col, topic_name)
579
579
  return t(keys, { defaultValue: col.header || col.id });
580
580
  }
581
581
 
582
+ /******************************************************************
583
+ * Column-title formatter: render the header as a span carrying
584
+ * `data-i18n` (the shared col id) so a live language switch —
585
+ * which calls refresh_language(document.body) — retranslates the
586
+ * Tabulator headers in place, without rebuilding the table. Only
587
+ * tag translatable columns; otherwise keep the schema header
588
+ * (refresh_language would otherwise overwrite it with the raw id).
589
+ * Tabulator re-runs this on redraw, so the full cascade re-applies.
590
+ ******************************************************************/
591
+ function col_title_formatter(gobj, col)
592
+ {
593
+ let topic_name = gobj_read_str_attr(gobj, "topic_name");
594
+ return function(cell, formatterParams, onRendered) {
595
+ let text = col_label(col, topic_name);
596
+ let attrs = {};
597
+ const NO_I18N = "\x00";
598
+ if(t(col.id, {defaultValue: NO_I18N}) !== NO_I18N) {
599
+ attrs.i18n = col.id;
600
+ }
601
+ return createElement2(['span', attrs, text]);
602
+ };
603
+ }
604
+
582
605
  /******************************************************************
583
606
  * Build table with Tabulator
584
607
  * This fn is called on start and when desc attribute is set.
@@ -717,6 +740,7 @@ function create_tabulator(gobj)
717
740
 
718
741
  let colDef = {
719
742
  title: col_label(col, gobj_read_str_attr(gobj, "topic_name")),
743
+ titleFormatter: col_title_formatter(gobj, col),
720
744
  field: col.id,
721
745
  sorter: sorter,
722
746
  hozAlign: hozAlign,
@@ -787,7 +811,21 @@ function create_tabulator(gobj)
787
811
  "color:#6b7280;padding:0 0.6rem;'></span>",
788
812
  });
789
813
 
790
- let tabulator = new Tabulator(`#${table_id}`, tabulator_settings);
814
+ /*
815
+ * Attach by ELEMENT, resolved inside OUR $container — a bare `#id`
816
+ * selector needs the element to be in the document already (a view
817
+ * built before being mounted crashes Tabulator: "no element found",
818
+ * then .on() dies on externalEvents null) and is shadowed by any
819
+ * stale duplicate id elsewhere in the page.
820
+ */
821
+ let $view_container = gobj_read_attr(gobj, "$container");
822
+ let $table_el = $view_container ?
823
+ $view_container.querySelector(`#${table_id}`) : null;
824
+ if(!$table_el) {
825
+ log_error(`${gobj_short_name(gobj)}: table element '#${table_id}' not found in $container`);
826
+ return;
827
+ }
828
+ let tabulator = new Tabulator($table_el, tabulator_settings);
791
829
 
792
830
  /* Keep the footer in sync with the visible (active) row count. */
793
831
  function update_rowcount() {
@@ -987,844 +1025,211 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
987
1025
  return value;
988
1026
  }
989
1027
 
990
- /******************************************************************
991
- * Build the topic form with bulma and select2
992
- ******************************************************************/
993
- function build_topic_form(gobj)
1028
+ /************************************************************
1029
+ * Build the form template from desc.cols: only user-editable
1030
+ * fields reach the form — writable cols, fkeys (linkable) and
1031
+ * the pkey (the hosted C_YUI_FORM's form_mode drives the
1032
+ * pkey's readonly/required state).
1033
+ ************************************************************/
1034
+ function build_form_template(gobj)
994
1035
  {
995
1036
  let desc = gobj_read_attr(gobj, "desc");
996
- let form_id = gobj_read_str_attr(gobj, "form_id");
997
- let $form = createElement2(['form', {id: form_id, class: 'box p-3', novalidate: ''}]);
998
-
999
- for (let i = 0; i < desc.cols.length; i++) {
1000
- let col = desc.cols[i];
1001
- if(col.id.charAt(0)==='_') {
1037
+ let template = [];
1038
+ for(let col of desc.cols) {
1039
+ if(!col.id || col.id.charAt(0) === '_') {
1002
1040
  continue;
1003
1041
  }
1004
- let [tag, form_input_type, options, extras] =
1005
- transform__treedb_type_2_form_type(gobj, col);
1006
-
1007
- if(!tag) {
1042
+ const field_desc = treedb_get_field_desc(col);
1043
+ if(field_desc.is_hidden) {
1008
1044
  continue;
1009
1045
  }
1010
- let id = col.id;
1011
- let flag = col.flag;
1012
-
1013
- let is_writable = str_in_list(flag, "writable");
1014
- let is_notnull = str_in_list(flag, "notnull");
1015
- let is_rowid = str_in_list(flag, "rowid");
1016
- let is_required = str_in_list(flag, "required");
1017
- let is_hidden = str_in_list(flag, "hidden");
1018
- let is_persistent = str_in_list(flag, "persistent");
1019
- let is_hook = str_in_list(flag, "hook");
1020
- let is_fkey = str_in_list(flag, "fkey");
1021
-
1022
- if(1) {
1023
- /*
1024
- * update mode by default
1025
- */
1026
- if(id === desc.pkey) {
1027
- is_writable = false;
1028
- }
1046
+ if(field_desc.type === "hook") {
1047
+ continue; // hooks don't appear in forms
1029
1048
  }
1030
-
1031
- // TODO chequea en backend que los writable no son actualizados desde fuera de programa
1032
- // if(is_persistent) {
1033
- // is_writable = true;
1034
- // }
1035
-
1036
- if(is_fkey) {
1037
- // TODO no debería estar definido con writable?
1038
- // TODO pueden haber enlaces que solo se puedan hacer por programa
1039
- is_writable = true;
1040
- }
1041
-
1042
- if(!is_writable) {
1043
- if(id !== desc.pkey) {
1044
- // Show only fields writable (except pkey)
1045
- continue;
1046
- }
1047
- }
1048
- if(is_hidden) {
1049
+ if(!field_desc.is_writable &&
1050
+ field_desc.type !== "fkey" && col.id !== desc.pkey) {
1049
1051
  continue;
1050
1052
  }
1051
-
1052
- if(is_rowid) {
1053
- is_writable = false;
1054
- is_required = false;
1055
- is_notnull = false;
1056
- }
1057
-
1058
- let field_conf = {
1059
- tag: tag,
1060
- inputType: form_input_type,
1061
- name: col.id,
1062
- label: col_label(col, gobj_read_str_attr(gobj, "topic_name")),
1063
- placeholder: "",
1064
- options: options, // select,select2,radio options
1065
- extras: extras, // extra html input/textarea attributes
1066
- readonly: !is_writable,
1067
- required: is_required,
1068
- hidden: is_hidden,
1069
- not_null: is_notnull,
1070
- is_pkey: (id === desc.pkey),
1071
- is_rowid: is_rowid
1072
- };
1073
-
1074
- let $field = create_html_field(field_conf);
1075
- $field.field_conf = field_conf;
1076
-
1077
- $form.appendChild($field);
1053
+ template.push(col);
1078
1054
  }
1079
- return $form;
1055
+ return template;
1080
1056
  }
1081
1057
 
1082
1058
  /************************************************************
1083
- * Convert a col type from backend to frontend
1084
- * Return data to build the HTMLElement
1085
- *
1086
- * Options format for select/select2:
1087
- * <option value="reading">Reading</option>
1059
+ * Collect the linkable parent rows for every fkey col:
1060
+ * {topic_name: rows}, asked to the topics manager (parent)
1061
+ * with the same command the old embedded form used. Collected
1062
+ * fresh on every dialog open, so new parent rows are always
1063
+ * offered.
1088
1064
  ************************************************************/
1089
- function transform__treedb_type_2_form_type(gobj, col)
1090
- {
1091
- const field_desc = treedb_get_field_desc(col);
1092
-
1093
- let tag = null;
1094
- let inputType = '';
1095
- let options = [];
1096
- let extras = {}; // extra html input/textarea attributes
1097
-
1098
- switch(field_desc.type) {
1099
- case "image":
1100
- case "string":
1101
- tag = 'input';
1102
- inputType = 'text';
1103
- break;
1104
- case "integer":
1105
- tag = 'input';
1106
- extras.inputmode = "numeric";
1107
- break;
1108
- case "real":
1109
- tag = 'input';
1110
- extras.inputmode = "decimal";
1111
- break;
1112
-
1113
- case "rowid":
1114
- tag = 'input';
1115
- inputType = 'text';
1116
- break;
1117
-
1118
- case "boolean":
1119
- tag = 'checkbox';
1120
- break;
1121
-
1122
- case "now":
1123
- case "time":
1124
- tag = 'input';
1125
- inputType = 'datetime-local';
1126
- switch(field_desc.real_type) {
1127
- case "string":
1128
- break;
1129
- case "integer":
1130
- break;
1131
- }
1132
- break;
1133
- case "color":
1134
- tag = 'input';
1135
- inputType = 'color';
1136
- switch(field_desc.real_type) {
1137
- case "string":
1138
- break;
1139
- case "integer":
1140
- break;
1141
- }
1142
- break;
1143
- case "email":
1144
- tag = 'input';
1145
- inputType = 'email';
1146
- extras.inputmode = "email";
1147
- break;
1148
- case "password":
1149
- tag = 'input';
1150
- inputType = 'password';
1151
- break;
1152
- case "url":
1153
- tag = 'input';
1154
- inputType = 'url';
1155
- extras.inputmode = "url";
1156
- break;
1157
- case "tel":
1158
- tag = 'input';
1159
- inputType = 'tel';
1160
- extras.inputmode = "tel";
1161
- break;
1162
-
1163
- case "object":
1164
- case "dict":
1165
- case "template":
1166
- case "array":
1167
- case "list":
1168
- case "coordinates":
1169
- case "blob":
1170
- tag = 'jsoneditor';
1171
- break;
1172
-
1173
- case "enum":
1174
- tag = 'select2';
1175
- switch(field_desc.real_type) {
1176
- case "string": // TODO review types
1177
- case "object":
1178
- case "dict":
1179
- case "array":
1180
- case "list":
1181
- options = list2options(field_desc.enum_list, "id", "id");
1182
- break;
1183
- }
1184
- break;
1185
-
1186
- case "hook":
1187
- // hooks don't appear in form
1188
- break;
1189
-
1190
- case "fkey":
1191
- tag = 'select2';
1192
- switch(field_desc.real_type) {
1193
- case "string": // TODO review types
1194
- case "object":
1195
- case "dict":
1196
- case "array":
1197
- case "list":
1198
- options = get_fkey_options(gobj, col);
1199
- break;
1200
- }
1201
- break;
1202
- }
1203
-
1204
- return [tag, inputType, options, extras];
1205
- }
1206
-
1207
- /******************************************************************
1208
- * Helper function to create different form elements
1209
- * Available types:
1210
- * - input
1211
- * - textarea
1212
- * - select
1213
- * - select2
1214
- * - checkbox
1215
- * - radio
1216
- * - jsoneditor
1217
- *
1218
- * {
1219
- * tag: field tag,
1220
- * inputType: input type,
1221
- * name: id
1222
- * label: ""
1223
- * placeholder: "",
1224
- * options: [{id:"", value:""}]]
1225
- * extras: {}
1226
- * readonly: bool
1227
- * required: bool
1228
- * hidden: bool
1229
- * not_null: bool
1230
- * _col: col
1231
- * }
1232
- ******************************************************************/
1233
- function create_html_field(
1234
- {
1235
- tag,
1236
- inputType,
1237
- name,
1238
- label,
1239
- placeholder,
1240
- options,
1241
- extras, // extra html input/textarea attributes
1242
- readonly,
1243
- required,
1244
- hidden,
1245
- not_null
1246
- }
1247
- )
1065
+ function build_fkey_options(gobj)
1248
1066
  {
1249
- let base;
1250
- switch (tag) {
1251
- case 'input':
1252
- case 'textarea':
1253
- case 'select':
1254
- case 'select2':
1255
- case 'checkbox':
1256
- case 'radio':
1257
- case 'jsoneditor':
1258
- base = [
1259
- 'div', {class: 'xfield field is-horizontal'}, [
1260
- ['div', {class: 'field-label is-normal' }, [
1261
- ['label', {class:'label ', i18n:label, for:name}, label]
1262
- ]],
1263
-
1264
- ['div', {class: 'field-body'}, [
1265
- ['div', {class: 'field'}, [
1266
- ['div', {class: 'control' }, []]
1267
- ]]
1268
- ]]
1269
- ]
1270
- ];
1271
- break;
1272
-
1273
- default:
1274
- base = ['div', {class: 'control'}, 'Unsupported element type'];
1275
- }
1276
-
1277
- let $element = createElement2(base);
1278
- let $control = $element.querySelector('.control');
1279
-
1280
- switch (tag) {
1281
- case 'input':
1282
- {
1283
- let attrs = {
1284
- class: `input ${name==='id'?'with-focus':''}`,
1285
- name: name,
1286
- type: inputType,
1287
- placeholder: placeholder
1288
- };
1289
- Object.assign(attrs, extras);
1290
-
1291
- if(readonly) {
1292
- attrs.readonly = '';
1293
- }
1294
- let extend = ['input', attrs, '', {
1295
- 'blur': function (evt) {
1296
- validate_field_on_blur(this);
1297
- }
1298
- }];
1299
-
1300
- let $extend = createElement2(extend);
1301
- $control.appendChild($extend);
1302
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1303
- break;
1304
- }
1305
-
1306
- case 'jsoneditor':
1307
- {
1308
- let attrs = {
1309
- class: 'jsoneditor jse-theme-dark',
1310
- style: ''
1311
- };
1312
- Object.assign(attrs, extras);
1313
-
1314
- if(readonly) {
1315
- attrs.readonly = '';
1316
- }
1317
- let extend = ['div', attrs];
1318
- let $extend = createElement2(extend);
1319
- $extend.setAttribute('name', name);
1320
- $control.appendChild($extend);
1321
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1322
- break;
1323
- }
1324
-
1325
- case 'textarea':
1326
- {
1327
- let attrs = {class:'textarea', name:name, placeholder: placeholder};
1328
- Object.assign(attrs, extras);
1329
-
1330
- if(readonly) {
1331
- attrs.readonly = '';
1332
- }
1333
- let extend = ['textarea', attrs, '', {
1334
- 'blur': function (evt) {
1335
- validate_field_on_blur(this);
1336
- }
1337
- }];
1338
- let $extend = createElement2(extend);
1339
- $control.appendChild($extend);
1340
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1341
- break;
1342
- }
1343
-
1344
- case 'select':
1345
- {
1346
- // {id:"id", value:"name"}
1347
- // <option value="id" data-i18n:"name"> name </option>
1348
- let extend = ['div', {class: 'select' },
1349
- ['select',
1350
- {name:name},
1351
- options.map(option =>
1352
- ['option', {value:option.id, i18n:option.value}, option.value]
1353
- )
1354
- ]
1355
- ];
1356
- let $extend = createElement2(extend);
1357
- $control.appendChild($extend);
1358
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1359
- break;
1360
- }
1361
-
1362
- case 'select2':
1363
- {
1364
- // {id:"id", value:"name"}
1365
- // <option value="id" data-i18n:"name"> name </option>
1366
- let extend = ['select',
1367
- {
1368
- name:name,
1369
- class: 'select2-multiple',
1370
- multiple:'multiple',
1371
- style: 'width: 100% !important;'
1372
- },
1373
- options.map(option =>
1374
- ['option', {value:option.id, i18n:option.value}, option.value]
1375
- )
1376
- ];
1377
- let $extend = createElement2(extend);
1378
- $control.appendChild($extend);
1379
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1380
- break;
1381
- }
1382
-
1383
- case 'checkbox':
1384
- {
1385
- let extend = ['label', {class: 'checkbox'},
1386
- ['input', {type: 'checkbox', name:name}]
1387
- ];
1388
- let $extend = createElement2(extend);
1389
- $control.appendChild($extend);
1390
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1391
- break;
1067
+ let desc = gobj_read_attr(gobj, "desc");
1068
+ let fkey_options = {};
1069
+ for(let col of desc.cols) {
1070
+ const field_desc = treedb_get_field_desc(col);
1071
+ if(field_desc.type !== "fkey" || !is_object(col.fkey)) {
1072
+ continue;
1392
1073
  }
1393
-
1394
- case 'radio':
1395
- {
1396
- for(const option of options) {
1397
- // WARNING here can't use data-i18n, the radio element is destroyed.
1398
- let elm = `
1399
- <label class="radio">
1400
- <input type="radio" name="${name}">
1401
- ${t(option)}
1402
- </label>
1403
- `;
1404
-
1405
- let $extend = createOneHtml(elm);
1406
- $control.appendChild($extend);
1407
- }
1408
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1409
- break;
1074
+ // HACK we work only with one fkey (same as the whole stack)
1075
+ let topic_name = Object.keys(col.fkey)[0];
1076
+ if(!topic_name || fkey_options[topic_name] !== undefined) {
1077
+ continue;
1410
1078
  }
1411
-
1412
- default:
1413
- log_error(`Unsupported element type, tag ${tag}`);
1079
+ let webix = gobj_command(
1080
+ gobj_parent(gobj),
1081
+ "get_topic_data",
1082
+ {topic_name: topic_name},
1083
+ gobj
1084
+ );
1085
+ fkey_options[topic_name] = (webix && is_array(webix.data))? webix.data : [];
1414
1086
  }
1415
-
1416
- return $element;
1087
+ return fkey_options;
1417
1088
  }
1418
1089
 
1419
- /******************************************************************
1420
- * Build topic form popup with bulma
1421
- ******************************************************************/
1422
- function build_topic_modal(gobj)
1090
+ /************************************************************
1091
+ * Open the edit/create dialog hosting a C_YUI_FORM child.
1092
+ * The child is created fresh on every open (schema and fkey
1093
+ * options are read at its build time) and destroyed on close.
1094
+ * Its EV_SAVE_RECORD (CHILD model) arrives already in treedb
1095
+ * shape — see ac_form_save_record().
1096
+ ************************************************************/
1097
+ function open_form_dialog(gobj, mode, record)
1423
1098
  {
1424
- const $form = build_topic_form(gobj);
1425
- let topic_name = gobj_read_str_attr(gobj, "topic_name");
1426
- let title = t(topic_name);
1099
+ close_form_dialog(gobj); // only one dialog at a time
1100
+
1101
+ let priv = gobj.priv;
1102
+ let desc = gobj_read_attr(gobj, "desc");
1427
1103
  let popup_id = gobj_read_str_attr(gobj, "popup_id");
1104
+ let topic_name = gobj_read_str_attr(gobj, "topic_name");
1428
1105
 
1429
1106
  let $element = createElement2([
1430
- 'div', {id: popup_id, class: 'modal' }, [
1431
- ['div', { class: 'modal-background' }, ''],
1432
- ['div', { class: 'modal-card modal-is-responsive' }, [
1433
- ['header', { class: 'modal-card-head p-3' }, [
1434
- ['p', { class: 'modal-card-title', style:'text-align:center;' }, title],
1435
- ['button', {class:"delete is-large", "aria-label":"close"}]
1436
- ]],
1437
- ['section', { class: 'modal-card-body p-1' }, [
1438
- $form
1107
+ 'div', {id: popup_id, class: 'modal is-active'}, [
1108
+ ['div', {class: 'modal-background'}, ''],
1109
+ ['div', {class: 'modal-card modal-is-responsive'}, [
1110
+ ['header', {class: 'modal-card-head p-3'}, [
1111
+ ['p', {class: 'modal-card-title', style: 'text-align:center;'}, t(topic_name)],
1112
+ ['button', {class: 'delete is-large', 'aria-label': 'close'}, '', {
1113
+ click: function(evt) {
1114
+ evt.stopPropagation();
1115
+ request_close_form_dialog(gobj);
1116
+ }
1117
+ }]
1439
1118
  ]],
1440
- ['footer', { class: 'modal-card-foot p-3' }, [
1441
- ['div', {class: 'buttons', style: 'justify-content:center; width:100%;'}, [
1442
- ['button', { class: 'button is-success px-4', type: 'submit'}, [
1443
- ['span', {class:'icon'}, [
1444
- ['i', {class:'yi-floppy-disk'}]
1445
- ]],
1446
- ['span', {i18n:'save'}, 'save']
1447
- ], {
1448
- click: function(evt) {
1449
- evt.stopPropagation();
1450
- evt.preventDefault();
1451
- let $form = $element.querySelector('form');
1452
- if (validate_form(gobj, $form)) {
1453
- let values = get_form_values($form);
1454
- let ret;
1455
- if($form.__mode__==="create") {
1456
- ret = gobj_send_event(gobj, "EV_CREATE_RECORD", values, gobj);
1457
- } else {
1458
- ret = gobj_send_event(gobj, "EV_UPDATE_RECORD", values, gobj);
1459
- }
1460
- if(ret === 0) {
1461
- $element.classList.remove('is-active');
1462
- }
1463
- }
1464
- }
1465
- }],
1466
-
1467
- // TODO disable/enable buttons save and undo accord to form state
1468
-
1469
- ['button', { class: 'button px-4 form-close'}, [
1470
- ['span', {class:'icon'}, [
1471
- ['i', {class:'yi-xmark'}]
1472
- ]],
1473
- ['span', {i18n:'cancel'}, 'cancel']
1474
- ]],
1475
-
1476
- ['button', { class: 'button px-4'}, [
1477
- ['span', {class:'icon'}, [
1478
- ['i', {class:'yi-broom'}]
1479
- ]],
1480
- ['span', {i18n:'clear'}, 'clear']
1481
- ], {
1482
- click: function(evt) {
1483
- evt.stopPropagation();
1484
- clear_validation(gobj, $form);
1485
- clear_data(gobj, $form);
1486
- }
1487
- }]
1488
- ]]
1489
- ]]
1119
+ /* flex column so the hosted form takes the dialog
1120
+ * height: fields scroll internally and the form's
1121
+ * bottom toolbar stays visible (dialog footer) */
1122
+ ['section', {
1123
+ class: 'modal-card-body p-2',
1124
+ style: 'display:flex; flex-direction:column;'
1125
+ }, []]
1490
1126
  ]]
1491
1127
  ], {
1492
- click: function (evt) {
1128
+ click: function(evt) {
1493
1129
  // Capture bubble events
1494
1130
  evt.stopPropagation();
1495
1131
  }
1496
1132
  }
1497
1133
  ]);
1498
1134
 
1499
- /*------------------------*
1500
- * Add events
1501
- *------------------------*/
1502
- /*
1503
- * Button close
1504
- */
1505
- $element.querySelectorAll('.modal-close, .form-close, .delete').forEach(function (element) {
1506
- element.addEventListener('click', function(event) {
1507
- event.stopPropagation();
1508
- // TODO ver si hay algo modificado y preguntar si se desecha
1509
- $element.classList.remove('is-active');
1510
- });
1511
- });
1512
-
1513
- refresh_language($element, t);
1514
-
1515
- /*-----------------------------------------*
1516
- * Add to popup layer !!!
1517
- *-----------------------------------------*/
1518
- popup_mount_layer().appendChild($element);
1519
-
1520
- // jQuery($element).off('click').on('click', function(evt) {
1521
- // evt.stopPropagation();
1522
- // evt.preventDefault();
1523
- // });
1524
-
1525
- /*------------------------*
1526
- * Jsoneditor
1527
- *------------------------*/
1528
- $element.querySelectorAll('.jsoneditor').forEach(function (element) {
1529
- let font_family = "DejaVu Sans Mono, monospace";
1530
- let sz = 16;
1531
- element.style.setProperty('--jse-font-size-mono', sz + 'px');
1532
- element.style.setProperty('--jse-font-family-mono', font_family);
1533
-
1534
- element.jsoneditor = new JSONEditor({
1535
- target: element,
1536
- props: {
1537
- timestampTag: function({field, value, path}) {
1538
- return field === '__t__' || field === '__tm__' || field === 'tm' ||
1539
- field === 'from_t' || field === 'to_t' || field === 't' ||
1540
- field === 't_input' || field === 't_output' ||
1541
- field === 'from_tm' || field === 'to_tm' || field === 'time';
1542
- },
1543
- }
1544
- });
1545
- });
1546
-
1547
- /*---------------------------------------------------------*
1548
- * Extensions of jquery must be done after DOM attaching
1549
- *---------------------------------------------------------*/
1550
- // let $$x = jQuery($element).find('.select2-multiple');
1551
- // if($$x && $$x.select2) {
1552
- // $$x.select2();
1553
- // }
1135
+ let $body = $element.querySelector('.modal-card-body');
1554
1136
 
1555
- $element.querySelectorAll(".select2-multiple").forEach((el) => {
1556
- if(el.tomselect) {
1557
- return; // already initialized
1558
- }
1559
- new TomSelect(el, {
1560
- plugins: ["remove_button"], // Optional: Adds a remove button for multiple selections
1561
- create: false, // Set to `true` if you want users to add new options
1562
- hideSelected: true,
1563
- closeAfterSelect: false
1564
- });
1565
- });
1566
-
1567
- return $element;
1568
- }
1569
-
1570
- /************************************************************
1571
- * Show a validation error on a field
1572
- ************************************************************/
1573
- function show_field_error($field, message)
1574
- {
1575
- $field.querySelectorAll('input, textarea, select').forEach(el => {
1576
- el.classList.add('is-danger');
1577
- });
1578
- let $help = $field.querySelector('.help');
1579
- if($help) {
1580
- $help.textContent = message;
1581
- $help.style.display = 'block';
1582
- }
1583
- }
1584
-
1585
- /************************************************************
1586
- * Clear a validation error from a field
1587
- ************************************************************/
1588
- function clear_field_error($field)
1589
- {
1590
- $field.querySelectorAll('input, textarea, select').forEach(el => {
1591
- el.classList.remove('is-danger');
1592
- });
1593
- let $help = $field.querySelector('.help');
1594
- if($help) {
1595
- $help.textContent = '';
1596
- $help.style.display = 'none';
1597
- }
1598
- }
1599
-
1600
- /************************************************************
1601
- * Get the current value of a field for validation
1602
- ************************************************************/
1603
- function get_field_value_for_validation($field, conf)
1604
- {
1605
- switch(conf.tag) {
1606
- case 'input': {
1607
- let $input = $field.querySelector('input');
1608
- if(!$input) {
1609
- return '';
1610
- }
1611
- if($input.type === 'checkbox') {
1612
- return $input.checked;
1613
- }
1614
- return $input.value;
1615
- }
1616
- case 'checkbox': {
1617
- let $input = $field.querySelector('input[type="checkbox"]');
1618
- if(!$input) {
1619
- return false;
1620
- }
1621
- return $input.checked;
1622
- }
1623
- case 'textarea': {
1624
- let $el = $field.querySelector('textarea');
1625
- if(!$el) {
1626
- return '';
1627
- }
1628
- return $el.value;
1629
- }
1630
- case 'select':
1631
- case 'select2': {
1632
- let $el = $field.querySelector('select');
1633
- if(!$el) {
1634
- return '';
1635
- }
1636
- if($el.tomselect) {
1637
- return $el.tomselect.getValue();
1638
- }
1639
- return $el.value;
1640
- }
1641
- case 'jsoneditor': {
1642
- let $el = $field.querySelector('.jsoneditor');
1643
- if(!$el || !$el.jsoneditor) {
1644
- return '';
1645
- }
1646
- let val = $el.jsoneditor.get();
1647
- if(val.text !== undefined) {
1648
- return val.text;
1649
- }
1650
- return JSON.stringify(val.json);
1651
- }
1652
- case 'radio': {
1653
- let $checked = $field.querySelector('input[type="radio"]:checked');
1654
- if(!$checked) {
1655
- return '';
1656
- }
1657
- return $checked.value;
1658
- }
1659
- }
1660
- return '';
1661
- }
1662
-
1663
- /************************************************************
1664
- * Check if a field value is empty
1665
- ************************************************************/
1666
- function is_field_value_empty(value)
1667
- {
1668
- if(value === null || value === undefined || value === '') {
1669
- return true;
1670
- }
1671
- if(Array.isArray(value) && value.length === 0) {
1672
- return true;
1673
- }
1674
- return false;
1675
- }
1676
-
1677
- /************************************************************
1678
- * Determine if a field requires validation in the current mode
1679
- ************************************************************/
1680
- function is_field_required(conf, mode)
1681
- {
1682
- if(conf.is_pkey) {
1683
- // pkey is required only in create mode (unless it's a rowid)
1684
- return (mode === "create" && !conf.is_rowid);
1685
- }
1686
- if(conf.readonly) {
1687
- return false;
1688
- }
1689
- return (conf.required || conf.not_null);
1690
- }
1691
-
1692
- /************************************************************
1693
- * Validate a single field on blur
1694
- * Called from input/textarea blur event handlers
1695
- ************************************************************/
1696
- function validate_field_on_blur(element)
1697
- {
1698
- let $field = element.closest('.xfield');
1699
- if(!$field || !$field.field_conf) {
1700
- return;
1701
- }
1702
- let conf = $field.field_conf;
1703
- let $form = element.closest('form');
1704
- let mode = $form ? $form.__mode__ : 'update';
1137
+ let form = gobj_create_pure_child(
1138
+ "form_" + clean_name(gobj_name(gobj)),
1139
+ "C_YUI_FORM",
1140
+ {
1141
+ template: build_form_template(gobj),
1142
+ record: record,
1143
+ fkey_options: build_fkey_options(gobj),
1144
+ form_mode: mode,
1145
+ pkey: desc.pkey || "id",
1146
+ topic_name: topic_name,
1147
+ editable: true,
1148
+ $parent: $body
1149
+ },
1150
+ gobj
1151
+ );
1152
+ priv.form = form;
1153
+ priv.$popup = $element;
1705
1154
 
1706
- if(!is_field_required(conf, mode)) {
1707
- clear_field_error($field);
1708
- return;
1155
+ let $form_container = gobj_read_attr(form, "$container");
1156
+ if($form_container) {
1157
+ $form_container.style.flex = "1 1 auto";
1158
+ $form_container.style.minHeight = "0";
1709
1159
  }
1710
1160
 
1711
- let value = get_field_value_for_validation($field, conf);
1712
- if(is_field_value_empty(value)) {
1713
- show_field_error($field, t('this field is required'));
1714
- } else {
1715
- clear_field_error($field);
1161
+ popup_mount_layer().appendChild($element);
1162
+ refresh_language($element, t);
1163
+
1164
+ gobj_start(form); // mt_start loads `record` into the form
1165
+
1166
+ let $with_focus = $element.querySelector('.with-focus');
1167
+ if($with_focus) {
1168
+ $with_focus.focus();
1716
1169
  }
1717
1170
  }
1718
1171
 
1719
1172
  /************************************************************
1720
- * Validate all required fields in the form
1721
- * Returns true if valid, false otherwise
1173
+ * Close request from the dialog X: if the form carries
1174
+ * unsaved changes ask first (EV_WINDOW_TO_CLOSE contract).
1722
1175
  ************************************************************/
1723
- function validate_form(gobj, $form)
1176
+ function request_close_form_dialog(gobj)
1724
1177
  {
1725
- let is_valid = true;
1726
- let first_invalid = null;
1727
- let mode = $form.__mode__ || 'update';
1728
-
1729
- $form.querySelectorAll('.xfield').forEach($field => {
1730
- let conf = $field.field_conf;
1731
- if(!conf) {
1732
- return;
1733
- }
1734
-
1735
- if(!is_field_required(conf, mode)) {
1736
- clear_field_error($field);
1178
+ let priv = gobj.priv;
1179
+ if(priv.form) {
1180
+ let kw = {};
1181
+ gobj_send_event(priv.form, "EV_WINDOW_TO_CLOSE", kw, gobj);
1182
+ if(kw.abort_close) {
1183
+ get_yesnocancel(t('All changes will be lost. Are you sure?'), function(evt) {
1184
+ if(evt === "yes") {
1185
+ close_form_dialog(gobj);
1186
+ }
1187
+ });
1737
1188
  return;
1738
1189
  }
1739
-
1740
- let value = get_field_value_for_validation($field, conf);
1741
- if(is_field_value_empty(value)) {
1742
- show_field_error($field, t('this field is required'));
1743
- is_valid = false;
1744
- if(!first_invalid) {
1745
- first_invalid = $field;
1746
- }
1747
- } else {
1748
- clear_field_error($field);
1749
- }
1750
- });
1751
-
1752
- if(first_invalid) {
1753
- let $focusable = first_invalid.querySelector('input, select, textarea');
1754
- if($focusable && $focusable.focus) {
1755
- $focusable.focus();
1756
- }
1757
1190
  }
1758
-
1759
- return is_valid;
1191
+ close_form_dialog(gobj);
1760
1192
  }
1761
1193
 
1762
1194
  /************************************************************
1763
- * Clear all validation errors from the form
1195
+ * Destroy the hosted form and remove the dialog DOM.
1764
1196
  ************************************************************/
1765
- function clear_validation(gobj, $form)
1197
+ function close_form_dialog(gobj)
1766
1198
  {
1767
- $form.querySelectorAll('.xfield').forEach($field => {
1768
- clear_field_error($field);
1769
- });
1199
+ let priv = gobj.priv;
1200
+ if(priv.form) {
1201
+ if(gobj_is_running(priv.form)) {
1202
+ gobj_stop(priv.form);
1203
+ }
1204
+ gobj_destroy(priv.form);
1205
+ priv.form = null;
1206
+ }
1207
+ if(priv.$popup) {
1208
+ if(priv.$popup.parentNode) {
1209
+ priv.$popup.parentNode.removeChild(priv.$popup);
1210
+ }
1211
+ priv.$popup = null;
1212
+ }
1770
1213
  }
1771
1214
 
1772
1215
  /************************************************************
1773
- * Before show edit/create from,
1774
- * set form state "update" or "create"
1775
- * Only the "id" field is modified,
1776
- * in "update" is readonly
1777
- * in "create" is writable
1778
- * Although by default the pkey (primary key) is "id",
1779
- * use always desc.pkey value, who knows all cases!
1216
+ * True if the topic's pkey col carries the "rowid" flag.
1780
1217
  ************************************************************/
1781
- function set_form_mode(gobj, $form, mode, index)
1218
+ function pkey_is_rowid(gobj)
1782
1219
  {
1783
1220
  let desc = gobj_read_attr(gobj, "desc");
1784
1221
  let col_pkey = kwid_find_one_record(
1785
1222
  gobj,
1786
1223
  desc.cols, // kw
1787
- null, // ids
1788
- { // jn_filter
1224
+ null, // ids
1225
+ { // jn_filter
1789
1226
  "id": desc.pkey
1790
1227
  }
1791
1228
  );
1792
- let is_rowid = str_in_list(col_pkey.flag, "rowid");
1793
-
1794
- let $id = $form.querySelector(`[name="${desc.pkey}"]`);
1795
-
1796
- if($id) {
1797
- if(mode === "update") {
1798
- /*
1799
- * update mode
1800
- */
1801
- $id.setAttribute('readonly', 'readonly');
1802
- $id.removeAttribute('required');
1803
- } else {
1804
- /*
1805
- * create mode
1806
- */
1807
- if(is_rowid) {
1808
- $id.setAttribute('readonly', 'readonly');
1809
- $id.removeAttribute('required');
1810
- } else {
1811
- $id.removeAttribute('readonly');
1812
- $id.setAttribute('required', '');
1813
- }
1814
- }
1815
- } else {
1816
- log_error(sprintf("%s: form without id field ('%s')",
1817
- gobj_short_name(gobj),
1818
- desc.pkey
1819
- ));
1820
- }
1821
-
1822
- if(is_rowid) {
1823
- mode = "create";
1229
+ if(!col_pkey) {
1230
+ return false;
1824
1231
  }
1825
-
1826
- $form.__mode__ = mode;
1827
- $form.__index__ = index;
1232
+ return str_in_list(col_pkey.flag, "rowid");
1828
1233
  }
1829
1234
 
1830
1235
  /************************************************************
@@ -1833,35 +1238,10 @@ function set_form_mode(gobj, $form, mode, index)
1833
1238
  ************************************************************/
1834
1239
  function show_edit_form(gobj, row, index)
1835
1240
  {
1836
- let popup_id = gobj_read_str_attr(gobj, "popup_id");
1837
- let form_id = gobj_read_str_attr(gobj, "form_id");
1838
-
1839
- let $element = document.getElementById(popup_id);
1840
- if(!$element) {
1841
- $element = build_topic_modal(gobj);
1842
- }
1843
-
1844
- let $form = document.getElementById(form_id);
1845
- clear_validation(gobj,$form);
1846
-
1847
- if(json_size(row) === 0 || // WARNING _check_box_state_ widely used
1848
- json_size(row) === 1 && ('_check_box_state_' in row)) {
1849
- /*
1850
- * WARNING it won't pass by here,
1851
- * "create" is call directly from Top New button
1852
- */
1853
- set_form_mode(gobj, $form, "create", index);
1854
- } else {
1855
- set_form_mode(gobj, $form, "update", index);
1856
- }
1857
-
1858
- set_form_values(gobj, $form, row);
1859
-
1860
- $element.classList.add('is-active');
1861
- let $with_focus = $element.querySelector('.with-focus');
1862
- if($with_focus) {
1863
- $with_focus.focus();
1864
- }
1241
+ /* a rowid pkey has no "update": every save appends a new
1242
+ * instance (timeranger semantics) open in create mode */
1243
+ let mode = pkey_is_rowid(gobj)? "create" : "update";
1244
+ open_form_dialog(gobj, mode, row);
1865
1245
  }
1866
1246
 
1867
1247
  /************************************************************
@@ -1870,24 +1250,7 @@ function show_edit_form(gobj, row, index)
1870
1250
  ************************************************************/
1871
1251
  function show_create_form(gobj, row)
1872
1252
  {
1873
- let popup_id = gobj_read_str_attr(gobj, "popup_id");
1874
- let form_id = gobj_read_str_attr(gobj, "form_id");
1875
-
1876
- let $element = document.getElementById(popup_id);
1877
- if(!$element) {
1878
- $element = build_topic_modal(gobj);
1879
- }
1880
-
1881
- let $form = document.getElementById(form_id);
1882
- clear_validation(gobj,$form);
1883
- set_form_mode(gobj, $form, "create", -1);
1884
- set_form_values(gobj, $form, row);
1885
-
1886
- $element.classList.add('is-active');
1887
- let $with_focus = $element.querySelector('.with-focus');
1888
- if($with_focus) {
1889
- $with_focus.focus();
1890
- }
1253
+ open_form_dialog(gobj, "create", row);
1891
1254
  }
1892
1255
 
1893
1256
  /************************************************************
@@ -1905,282 +1268,6 @@ function get_schema_col(gobj, id)
1905
1268
  return null;
1906
1269
  }
1907
1270
 
1908
- /************************************************************
1909
- *
1910
- ************************************************************/
1911
- function clear_data(gobj, $form)
1912
- {
1913
- $form.querySelectorAll('input, select, textarea, .jsoneditor').forEach(input => {
1914
- if(input.name !== 'id') {
1915
- if (input.classList.contains('jsoneditor')) {
1916
- let jsoneditor = input.jsoneditor;
1917
- if(jsoneditor) {
1918
- jsoneditor.set({text: ''});
1919
- }
1920
- } else if (input.type === 'checkbox') {
1921
- input.checked = false;
1922
- } else if (input.type === 'radio') {
1923
- input.checked = false;
1924
-
1925
- } else if (input.type === 'datetime-local') { /* time */
1926
- input.value = null;
1927
-
1928
- } else if (input.multiple) {
1929
- if(input.tomselect) {
1930
- input.tomselect.clear();
1931
- }
1932
- } else {
1933
- input.value = null;
1934
- }
1935
- }
1936
- });
1937
- }
1938
-
1939
- /************************************************************
1940
- *
1941
- ************************************************************/
1942
- function get_form_values($form)
1943
- {
1944
- const values = {};
1945
- // Process all form elements
1946
- $form.querySelectorAll('input, select, textarea, .jsoneditor').forEach(input => {
1947
- if (input.classList.contains('jsoneditor')) {
1948
- let jsoneditor = input.jsoneditor;
1949
- if(jsoneditor) {
1950
- let name = input.getAttribute('name');
1951
- let value = jsoneditor.get();
1952
- if(value.text===undefined) {
1953
- values[name] = JSON.stringify(value.json, null, null);
1954
- } else {
1955
- values[name] = value.text;
1956
- }
1957
- }
1958
- } else if (input.type === 'checkbox') {
1959
- // Handle single checkbox with true/false values
1960
- values[input.name] = input.checked;
1961
- } else if (input.type === 'radio') {
1962
- // Handle radio buttons
1963
- if (input.checked) {
1964
- values[input.name] = input.value;
1965
- }
1966
-
1967
- } else if (input.type === 'datetime-local') { /* time */
1968
- // Create a new Date object from the datetime-local input value
1969
- const date = new Date(input.value);
1970
-
1971
- // Convert the Date object to epoch time (in seconds)
1972
- const epochTime = Math.floor(date.getTime() / 1000);
1973
- values[input.name] = epochTime;
1974
-
1975
- } else if (input.multiple) {
1976
- // Handle multi-select with Select2
1977
- // values[input.name] = $(input).val();
1978
-
1979
- // Handle multi-select with Tom-Select
1980
- if (input.tomselect) {
1981
- values[input.name] = input.tomselect.getValue(); // Returns an array of selected values
1982
- }
1983
- } else {
1984
- // Handle other input types (text, email, number, select, textarea)
1985
- values[input.name] = input.value;
1986
- }
1987
- });
1988
-
1989
- return values;
1990
- }
1991
-
1992
- /************************************************************
1993
- * Convert a record column value from backend to frontend
1994
- ************************************************************/
1995
- function epochToDateTimeLocal(epochTime)
1996
- {
1997
- const date = new Date(epochTime * 1000); // Convert to milliseconds
1998
- const year = date.getFullYear();
1999
- const month = String(date.getMonth() + 1).padStart(2, '0');
2000
- const day = String(date.getDate()).padStart(2, '0');
2001
- const hours = String(date.getHours()).padStart(2, '0');
2002
- const minutes = String(date.getMinutes()).padStart(2, '0');
2003
- const seconds = String(date.getSeconds()).padStart(2, '0');
2004
-
2005
- // TODO for 'datetime-local' input, there is no seconds! we must use another library
2006
- return `${year}-${month}-${day}T${hours}:${minutes}`;
2007
- }
2008
-
2009
- /************************************************************
2010
- *
2011
- ************************************************************/
2012
- function set_form_values(gobj, form, values)
2013
- {
2014
- Object.keys(values).forEach(key => {
2015
- let element = form.querySelector(`[name="${key}"]`);
2016
- if (element) {
2017
- let value = values[key];
2018
- let col = get_schema_col(gobj, key);
2019
- if(col) {
2020
- value = transform__treedb_value_2_form_value(gobj, col, value);
2021
- }
2022
-
2023
- switch (element.type) {
2024
- case 'select':
2025
- element.value = value;
2026
- break;
2027
-
2028
- case 'select-one':
2029
- element.value = value;
2030
- break;
2031
-
2032
- case 'select-multiple':
2033
- // Handle multi-select with Select2
2034
- //$(element).val(value).trigger('change');
2035
-
2036
- // Handle multi-select with Tom-Select
2037
- if (element.tomselect) {
2038
- element.tomselect.setValue(value);
2039
- }
2040
- break;
2041
-
2042
- case 'radio':
2043
- // TODO review
2044
- let radio = form.querySelector(`input[name="${key}"][value="${values[key]}"]`);
2045
- if (radio) {
2046
- radio.checked = true;
2047
- }
2048
- break;
2049
-
2050
- case 'checkbox':
2051
- element.checked = value; // Assume the value is true or false
2052
- break;
2053
-
2054
- case 'text':
2055
- case 'textarea':
2056
- case 'email':
2057
- element.value = value;
2058
- break;
2059
-
2060
- case 'datetime-local': /* time */
2061
- element.value = epochToDateTimeLocal(value);
2062
- break;
2063
-
2064
- default:
2065
- if(element.classList.contains('jsoneditor')) {
2066
- let jsoneditor = element.jsoneditor;
2067
- if(jsoneditor) {
2068
- jsoneditor.set(value);
2069
- }
2070
- break;
2071
- }
2072
-
2073
- element.value = value;
2074
- log_error(
2075
- sprintf("%s: form type unknown: '%s'",
2076
- gobj_short_name(gobj),
2077
- element.type
2078
- )
2079
- );
2080
- break;
2081
- }
2082
- }
2083
- });
2084
- }
2085
-
2086
- /************************************************************
2087
- * Convert a record column value from backend
2088
- * to frontend form field
2089
- ************************************************************/
2090
- function transform__treedb_value_2_form_value(gobj, col, value)
2091
- {
2092
- const field_desc = treedb_get_field_desc(col);
2093
-
2094
- switch(field_desc.type) {
2095
- case "string":
2096
- case "email": // string subtype
2097
- case "tel": // string subtype
2098
- case "url": // string subtype
2099
- break;
2100
- case "integer":
2101
- break;
2102
- case "real":
2103
- break;
2104
- case "boolean":
2105
- break;
2106
-
2107
- case "object":
2108
- case "dict":
2109
- case "template":
2110
- case "array":
2111
- case "list":
2112
- case "coordinates":
2113
- case "blob":
2114
- // Return as it is, manage by jsoneditor
2115
- value = {json: value};
2116
- // value = {text: JSON.stringify(value)}; // other way
2117
- break;
2118
-
2119
- case "enum":
2120
- switch(field_desc.real_type) {
2121
- case "string":
2122
- break;
2123
- case "object":
2124
- case "dict":
2125
- case "array":
2126
- case "list":
2127
- break;
2128
- }
2129
- break;
2130
-
2131
- case "now":
2132
- case "time":
2133
- switch(field_desc.real_type) {
2134
- case "string":
2135
- case "integer":
2136
- value = parseInt(value) || 0;
2137
- value = new Date(value);
2138
- break;
2139
- }
2140
- break;
2141
-
2142
- case "color":
2143
- switch(field_desc.real_type) {
2144
- case "string":
2145
- // TODO
2146
- break;
2147
- case "integer":
2148
- // TODO
2149
- break;
2150
- }
2151
- break;
2152
-
2153
- case "hook": // Convert data from backend to frontend FORM FIELD
2154
- // WARNING hook fields are not writable and by now they are not showed in forms.
2155
- break;
2156
-
2157
- case "fkey": // Convert data from backend to frontend FORM FIELD
2158
- let new_value = [];
2159
- if(value) {
2160
- if(is_string(value)) {
2161
- let fkey = treedb_decoder_fkey(col, value);
2162
- if(fkey) {
2163
- new_value.push(fkey.id);
2164
- }
2165
- } else if(is_array(value)) {
2166
- for(let i=0; i<value.length; i++) {
2167
- let fkey = treedb_decoder_fkey(col, value[i]);
2168
- if(fkey) {
2169
- new_value.push(fkey.id);
2170
- }
2171
- }
2172
- } else {
2173
- log_error("fkey type unsupported: " + JSON.stringify(value));
2174
- }
2175
- }
2176
-
2177
- value = new_value;
2178
- break;
2179
- }
2180
-
2181
- return value;
2182
- }
2183
-
2184
1271
  /************************************************************
2185
1272
  * Convert from frontend to backend
2186
1273
  * operation: "create" "update"
@@ -2360,24 +1447,6 @@ function transform__form_value_2_treedb_value(gobj, col, value, operation)
2360
1447
  return value;
2361
1448
  }
2362
1449
 
2363
- /************************************************************
2364
- *
2365
- ************************************************************/
2366
- function get_fkey_options(gobj, col)
2367
- {
2368
- for(let topic_name of Object.keys(col.fkey)) {
2369
- // HACK we work only with one fkey
2370
- let kw = {
2371
- topic_name: topic_name
2372
- };
2373
- let webix = gobj_command(gobj_parent(gobj), "get_topic_data", kw, gobj);
2374
- let current_list = webix.data;
2375
- return list2options(current_list, "id", "id");
2376
- }
2377
- log_error("No fkey options found" + JSON.stringify(col));
2378
- return null;
2379
- }
2380
-
2381
1450
  /************************************************************
2382
1451
  *
2383
1452
  ************************************************************/
@@ -2917,58 +1986,29 @@ function ac_paste_rows(gobj, event, kw, src)
2917
1986
  }
2918
1987
 
2919
1988
  /************************************************************
2920
- * From internal Save button of "create" modal form
1989
+ * EV_SAVE_RECORD from the hosted C_YUI_FORM: kw is the
1990
+ * record already in treedb shape (the form itself encodes
1991
+ * fkeys, times and numbers). Route by the form's mode.
2921
1992
  ************************************************************/
2922
- function ac_create_record(gobj, event, kw, src)
1993
+ function ac_form_save_record(gobj, event, kw, src)
2923
1994
  {
2924
- let new_kw = null;
2925
- try {
2926
- new_kw = transform__form_record_2_treedb_record(gobj, kw, "create");
2927
- } catch (e) {
2928
- log_warning(e);
2929
- throw e;
2930
- }
2931
-
1995
+ let mode = gobj_read_str_attr(src, "form_mode");
2932
1996
  gobj_publish_event(
2933
1997
  gobj,
2934
- "EV_CREATE_RECORD",
1998
+ (mode === "create")? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD",
2935
1999
  {
2936
2000
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
2937
- record: new_kw
2001
+ record: kw
2938
2002
  }
2939
2003
  );
2940
2004
 
2941
- /*
2942
- * Return -1 will not close the form
2943
- */
2944
- return 0;
2945
- }
2946
-
2947
- /************************************************************
2948
- * From internal Save button of "update" modal form
2949
- ************************************************************/
2950
- function ac_update_record(gobj, event, kw, src)
2951
- {
2952
- let new_kw = null;
2953
- try {
2954
- new_kw = transform__form_record_2_treedb_record(gobj, kw, "update");
2955
- } catch (e) {
2956
- log_warning(e);
2957
- throw e;
2958
- }
2959
-
2960
- gobj_publish_event(
2961
- gobj,
2962
- "EV_UPDATE_RECORD",
2963
- {
2964
- topic_name: gobj_read_str_attr(gobj, "topic_name"),
2965
- record: new_kw
2966
- }
2967
- );
2005
+ /* we are INSIDE the form's gobj_publish_event stack — never
2006
+ * destroy the publisher synchronously from a subscriber
2007
+ * callback: defer the close */
2008
+ setTimeout(function() {
2009
+ close_form_dialog(gobj);
2010
+ }, 0);
2968
2011
 
2969
- /*
2970
- * Return -1 will not close the form
2971
- */
2972
2012
  return 0;
2973
2013
  }
2974
2014
 
@@ -3147,8 +2187,7 @@ function create_gclass(gclass_name)
3147
2187
  ["EV_NODE_DELETED", ac_node_deleted, null],
3148
2188
 
3149
2189
  ["EV_EDITION_MODE", ac_edition_mode, null],
3150
- ["EV_CREATE_RECORD", ac_create_record, null],
3151
- ["EV_UPDATE_RECORD", ac_update_record, null],
2190
+ ["EV_SAVE_RECORD", ac_form_save_record, null],
3152
2191
 
3153
2192
  ["EV_NEW_ROW", ac_new_row, null],
3154
2193
  ["EV_DELETE_ROWS", ac_delete_rows, null],
@@ -3174,6 +2213,7 @@ function create_gclass(gclass_name)
3174
2213
  ["EV_NODE_DELETED", 0],
3175
2214
 
3176
2215
  ["EV_EDITION_MODE", 0],
2216
+ ["EV_SAVE_RECORD", 0],
3177
2217
  ["EV_NEW_ROW", 0],
3178
2218
  ["EV_DELETE_ROWS", 0],
3179
2219
  ["EV_COPY_ROWS", 0],
@@ -3219,6 +2259,11 @@ function create_gclass(gclass_name)
3219
2259
  ***************************************************************/
3220
2260
  function register_c_yui_treedb_topic_with_form()
3221
2261
  {
2262
+ /* the edit/create dialog hosts a C_YUI_FORM child: make sure
2263
+ * its gclass exists even if the app didn't register it */
2264
+ if(!gclass_find_by_name("C_YUI_FORM", false)) {
2265
+ register_c_yui_form();
2266
+ }
3222
2267
  return create_gclass(GCLASS_NAME);
3223
2268
  }
3224
2269