@yuneta/gobj-ui 2.2.5 → 2.3.1

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,
@@ -1001,844 +1025,211 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
1001
1025
  return value;
1002
1026
  }
1003
1027
 
1004
- /******************************************************************
1005
- * Build the topic form with bulma and select2
1006
- ******************************************************************/
1007
- 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)
1008
1035
  {
1009
1036
  let desc = gobj_read_attr(gobj, "desc");
1010
- let form_id = gobj_read_str_attr(gobj, "form_id");
1011
- let $form = createElement2(['form', {id: form_id, class: 'box p-3', novalidate: ''}]);
1012
-
1013
- for (let i = 0; i < desc.cols.length; i++) {
1014
- let col = desc.cols[i];
1015
- if(col.id.charAt(0)==='_') {
1037
+ let template = [];
1038
+ for(let col of desc.cols) {
1039
+ if(!col.id || col.id.charAt(0) === '_') {
1016
1040
  continue;
1017
1041
  }
1018
- let [tag, form_input_type, options, extras] =
1019
- transform__treedb_type_2_form_type(gobj, col);
1020
-
1021
- if(!tag) {
1042
+ const field_desc = treedb_get_field_desc(col);
1043
+ if(field_desc.is_hidden) {
1022
1044
  continue;
1023
1045
  }
1024
- let id = col.id;
1025
- let flag = col.flag;
1026
-
1027
- let is_writable = str_in_list(flag, "writable");
1028
- let is_notnull = str_in_list(flag, "notnull");
1029
- let is_rowid = str_in_list(flag, "rowid");
1030
- let is_required = str_in_list(flag, "required");
1031
- let is_hidden = str_in_list(flag, "hidden");
1032
- let is_persistent = str_in_list(flag, "persistent");
1033
- let is_hook = str_in_list(flag, "hook");
1034
- let is_fkey = str_in_list(flag, "fkey");
1035
-
1036
- if(1) {
1037
- /*
1038
- * update mode by default
1039
- */
1040
- if(id === desc.pkey) {
1041
- is_writable = false;
1042
- }
1046
+ if(field_desc.type === "hook") {
1047
+ continue; // hooks don't appear in forms
1043
1048
  }
1044
-
1045
- // TODO chequea en backend que los writable no son actualizados desde fuera de programa
1046
- // if(is_persistent) {
1047
- // is_writable = true;
1048
- // }
1049
-
1050
- if(is_fkey) {
1051
- // TODO no debería estar definido con writable?
1052
- // TODO pueden haber enlaces que solo se puedan hacer por programa
1053
- is_writable = true;
1054
- }
1055
-
1056
- if(!is_writable) {
1057
- if(id !== desc.pkey) {
1058
- // Show only fields writable (except pkey)
1059
- continue;
1060
- }
1061
- }
1062
- if(is_hidden) {
1049
+ if(!field_desc.is_writable &&
1050
+ field_desc.type !== "fkey" && col.id !== desc.pkey) {
1063
1051
  continue;
1064
1052
  }
1065
-
1066
- if(is_rowid) {
1067
- is_writable = false;
1068
- is_required = false;
1069
- is_notnull = false;
1070
- }
1071
-
1072
- let field_conf = {
1073
- tag: tag,
1074
- inputType: form_input_type,
1075
- name: col.id,
1076
- label: col_label(col, gobj_read_str_attr(gobj, "topic_name")),
1077
- placeholder: "",
1078
- options: options, // select,select2,radio options
1079
- extras: extras, // extra html input/textarea attributes
1080
- readonly: !is_writable,
1081
- required: is_required,
1082
- hidden: is_hidden,
1083
- not_null: is_notnull,
1084
- is_pkey: (id === desc.pkey),
1085
- is_rowid: is_rowid
1086
- };
1087
-
1088
- let $field = create_html_field(field_conf);
1089
- $field.field_conf = field_conf;
1090
-
1091
- $form.appendChild($field);
1053
+ template.push(col);
1092
1054
  }
1093
- return $form;
1055
+ return template;
1094
1056
  }
1095
1057
 
1096
1058
  /************************************************************
1097
- * Convert a col type from backend to frontend
1098
- * Return data to build the HTMLElement
1099
- *
1100
- * Options format for select/select2:
1101
- * <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.
1102
1064
  ************************************************************/
1103
- function transform__treedb_type_2_form_type(gobj, col)
1104
- {
1105
- const field_desc = treedb_get_field_desc(col);
1106
-
1107
- let tag = null;
1108
- let inputType = '';
1109
- let options = [];
1110
- let extras = {}; // extra html input/textarea attributes
1111
-
1112
- switch(field_desc.type) {
1113
- case "image":
1114
- case "string":
1115
- tag = 'input';
1116
- inputType = 'text';
1117
- break;
1118
- case "integer":
1119
- tag = 'input';
1120
- extras.inputmode = "numeric";
1121
- break;
1122
- case "real":
1123
- tag = 'input';
1124
- extras.inputmode = "decimal";
1125
- break;
1126
-
1127
- case "rowid":
1128
- tag = 'input';
1129
- inputType = 'text';
1130
- break;
1131
-
1132
- case "boolean":
1133
- tag = 'checkbox';
1134
- break;
1135
-
1136
- case "now":
1137
- case "time":
1138
- tag = 'input';
1139
- inputType = 'datetime-local';
1140
- switch(field_desc.real_type) {
1141
- case "string":
1142
- break;
1143
- case "integer":
1144
- break;
1145
- }
1146
- break;
1147
- case "color":
1148
- tag = 'input';
1149
- inputType = 'color';
1150
- switch(field_desc.real_type) {
1151
- case "string":
1152
- break;
1153
- case "integer":
1154
- break;
1155
- }
1156
- break;
1157
- case "email":
1158
- tag = 'input';
1159
- inputType = 'email';
1160
- extras.inputmode = "email";
1161
- break;
1162
- case "password":
1163
- tag = 'input';
1164
- inputType = 'password';
1165
- break;
1166
- case "url":
1167
- tag = 'input';
1168
- inputType = 'url';
1169
- extras.inputmode = "url";
1170
- break;
1171
- case "tel":
1172
- tag = 'input';
1173
- inputType = 'tel';
1174
- extras.inputmode = "tel";
1175
- break;
1176
-
1177
- case "object":
1178
- case "dict":
1179
- case "template":
1180
- case "array":
1181
- case "list":
1182
- case "coordinates":
1183
- case "blob":
1184
- tag = 'jsoneditor';
1185
- break;
1186
-
1187
- case "enum":
1188
- tag = 'select2';
1189
- switch(field_desc.real_type) {
1190
- case "string": // TODO review types
1191
- case "object":
1192
- case "dict":
1193
- case "array":
1194
- case "list":
1195
- options = list2options(field_desc.enum_list, "id", "id");
1196
- break;
1197
- }
1198
- break;
1199
-
1200
- case "hook":
1201
- // hooks don't appear in form
1202
- break;
1203
-
1204
- case "fkey":
1205
- tag = 'select2';
1206
- switch(field_desc.real_type) {
1207
- case "string": // TODO review types
1208
- case "object":
1209
- case "dict":
1210
- case "array":
1211
- case "list":
1212
- options = get_fkey_options(gobj, col);
1213
- break;
1214
- }
1215
- break;
1216
- }
1217
-
1218
- return [tag, inputType, options, extras];
1219
- }
1220
-
1221
- /******************************************************************
1222
- * Helper function to create different form elements
1223
- * Available types:
1224
- * - input
1225
- * - textarea
1226
- * - select
1227
- * - select2
1228
- * - checkbox
1229
- * - radio
1230
- * - jsoneditor
1231
- *
1232
- * {
1233
- * tag: field tag,
1234
- * inputType: input type,
1235
- * name: id
1236
- * label: ""
1237
- * placeholder: "",
1238
- * options: [{id:"", value:""}]]
1239
- * extras: {}
1240
- * readonly: bool
1241
- * required: bool
1242
- * hidden: bool
1243
- * not_null: bool
1244
- * _col: col
1245
- * }
1246
- ******************************************************************/
1247
- function create_html_field(
1248
- {
1249
- tag,
1250
- inputType,
1251
- name,
1252
- label,
1253
- placeholder,
1254
- options,
1255
- extras, // extra html input/textarea attributes
1256
- readonly,
1257
- required,
1258
- hidden,
1259
- not_null
1260
- }
1261
- )
1065
+ function build_fkey_options(gobj)
1262
1066
  {
1263
- let base;
1264
- switch (tag) {
1265
- case 'input':
1266
- case 'textarea':
1267
- case 'select':
1268
- case 'select2':
1269
- case 'checkbox':
1270
- case 'radio':
1271
- case 'jsoneditor':
1272
- base = [
1273
- 'div', {class: 'xfield field is-horizontal'}, [
1274
- ['div', {class: 'field-label is-normal' }, [
1275
- ['label', {class:'label ', i18n:label, for:name}, label]
1276
- ]],
1277
-
1278
- ['div', {class: 'field-body'}, [
1279
- ['div', {class: 'field'}, [
1280
- ['div', {class: 'control' }, []]
1281
- ]]
1282
- ]]
1283
- ]
1284
- ];
1285
- break;
1286
-
1287
- default:
1288
- base = ['div', {class: 'control'}, 'Unsupported element type'];
1289
- }
1290
-
1291
- let $element = createElement2(base);
1292
- let $control = $element.querySelector('.control');
1293
-
1294
- switch (tag) {
1295
- case 'input':
1296
- {
1297
- let attrs = {
1298
- class: `input ${name==='id'?'with-focus':''}`,
1299
- name: name,
1300
- type: inputType,
1301
- placeholder: placeholder
1302
- };
1303
- Object.assign(attrs, extras);
1304
-
1305
- if(readonly) {
1306
- attrs.readonly = '';
1307
- }
1308
- let extend = ['input', attrs, '', {
1309
- 'blur': function (evt) {
1310
- validate_field_on_blur(this);
1311
- }
1312
- }];
1313
-
1314
- let $extend = createElement2(extend);
1315
- $control.appendChild($extend);
1316
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1317
- break;
1318
- }
1319
-
1320
- case 'jsoneditor':
1321
- {
1322
- let attrs = {
1323
- class: 'jsoneditor jse-theme-dark',
1324
- style: ''
1325
- };
1326
- Object.assign(attrs, extras);
1327
-
1328
- if(readonly) {
1329
- attrs.readonly = '';
1330
- }
1331
- let extend = ['div', attrs];
1332
- let $extend = createElement2(extend);
1333
- $extend.setAttribute('name', name);
1334
- $control.appendChild($extend);
1335
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1336
- break;
1337
- }
1338
-
1339
- case 'textarea':
1340
- {
1341
- let attrs = {class:'textarea', name:name, placeholder: placeholder};
1342
- Object.assign(attrs, extras);
1343
-
1344
- if(readonly) {
1345
- attrs.readonly = '';
1346
- }
1347
- let extend = ['textarea', attrs, '', {
1348
- 'blur': function (evt) {
1349
- validate_field_on_blur(this);
1350
- }
1351
- }];
1352
- let $extend = createElement2(extend);
1353
- $control.appendChild($extend);
1354
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1355
- break;
1356
- }
1357
-
1358
- case 'select':
1359
- {
1360
- // {id:"id", value:"name"}
1361
- // <option value="id" data-i18n:"name"> name </option>
1362
- let extend = ['div', {class: 'select' },
1363
- ['select',
1364
- {name:name},
1365
- options.map(option =>
1366
- ['option', {value:option.id, i18n:option.value}, option.value]
1367
- )
1368
- ]
1369
- ];
1370
- let $extend = createElement2(extend);
1371
- $control.appendChild($extend);
1372
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1373
- break;
1374
- }
1375
-
1376
- case 'select2':
1377
- {
1378
- // {id:"id", value:"name"}
1379
- // <option value="id" data-i18n:"name"> name </option>
1380
- let extend = ['select',
1381
- {
1382
- name:name,
1383
- class: 'select2-multiple',
1384
- multiple:'multiple',
1385
- style: 'width: 100% !important;'
1386
- },
1387
- options.map(option =>
1388
- ['option', {value:option.id, i18n:option.value}, option.value]
1389
- )
1390
- ];
1391
- let $extend = createElement2(extend);
1392
- $control.appendChild($extend);
1393
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1394
- break;
1395
- }
1396
-
1397
- case 'checkbox':
1398
- {
1399
- let extend = ['label', {class: 'checkbox'},
1400
- ['input', {type: 'checkbox', name:name}]
1401
- ];
1402
- let $extend = createElement2(extend);
1403
- $control.appendChild($extend);
1404
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1405
- 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;
1406
1073
  }
1407
-
1408
- case 'radio':
1409
- {
1410
- for(const option of options) {
1411
- // WARNING here can't use data-i18n, the radio element is destroyed.
1412
- let elm = `
1413
- <label class="radio">
1414
- <input type="radio" name="${name}">
1415
- ${t(option)}
1416
- </label>
1417
- `;
1418
-
1419
- let $extend = createOneHtml(elm);
1420
- $control.appendChild($extend);
1421
- }
1422
- $control.appendChild(createElement2(['p', {class: 'help is-danger', style: 'display:none'}, '']));
1423
- 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;
1424
1078
  }
1425
-
1426
- default:
1427
- 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 : [];
1428
1086
  }
1429
-
1430
- return $element;
1087
+ return fkey_options;
1431
1088
  }
1432
1089
 
1433
- /******************************************************************
1434
- * Build topic form popup with bulma
1435
- ******************************************************************/
1436
- 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)
1437
1098
  {
1438
- const $form = build_topic_form(gobj);
1439
- let topic_name = gobj_read_str_attr(gobj, "topic_name");
1440
- 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");
1441
1103
  let popup_id = gobj_read_str_attr(gobj, "popup_id");
1104
+ let topic_name = gobj_read_str_attr(gobj, "topic_name");
1442
1105
 
1443
1106
  let $element = createElement2([
1444
- 'div', {id: popup_id, class: 'modal' }, [
1445
- ['div', { class: 'modal-background' }, ''],
1446
- ['div', { class: 'modal-card modal-is-responsive' }, [
1447
- ['header', { class: 'modal-card-head p-3' }, [
1448
- ['p', { class: 'modal-card-title', style:'text-align:center;' }, title],
1449
- ['button', {class:"delete is-large", "aria-label":"close"}]
1450
- ]],
1451
- ['section', { class: 'modal-card-body p-1' }, [
1452
- $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
+ }]
1453
1118
  ]],
1454
- ['footer', { class: 'modal-card-foot p-3' }, [
1455
- ['div', {class: 'buttons', style: 'justify-content:center; width:100%;'}, [
1456
- ['button', { class: 'button is-success px-4', type: 'submit'}, [
1457
- ['span', {class:'icon'}, [
1458
- ['i', {class:'yi-floppy-disk'}]
1459
- ]],
1460
- ['span', {i18n:'save'}, 'save']
1461
- ], {
1462
- click: function(evt) {
1463
- evt.stopPropagation();
1464
- evt.preventDefault();
1465
- let $form = $element.querySelector('form');
1466
- if (validate_form(gobj, $form)) {
1467
- let values = get_form_values($form);
1468
- let ret;
1469
- if($form.__mode__==="create") {
1470
- ret = gobj_send_event(gobj, "EV_CREATE_RECORD", values, gobj);
1471
- } else {
1472
- ret = gobj_send_event(gobj, "EV_UPDATE_RECORD", values, gobj);
1473
- }
1474
- if(ret === 0) {
1475
- $element.classList.remove('is-active');
1476
- }
1477
- }
1478
- }
1479
- }],
1480
-
1481
- // TODO disable/enable buttons save and undo accord to form state
1482
-
1483
- ['button', { class: 'button px-4 form-close'}, [
1484
- ['span', {class:'icon'}, [
1485
- ['i', {class:'yi-xmark'}]
1486
- ]],
1487
- ['span', {i18n:'cancel'}, 'cancel']
1488
- ]],
1489
-
1490
- ['button', { class: 'button px-4'}, [
1491
- ['span', {class:'icon'}, [
1492
- ['i', {class:'yi-broom'}]
1493
- ]],
1494
- ['span', {i18n:'clear'}, 'clear']
1495
- ], {
1496
- click: function(evt) {
1497
- evt.stopPropagation();
1498
- clear_validation(gobj, $form);
1499
- clear_data(gobj, $form);
1500
- }
1501
- }]
1502
- ]]
1503
- ]]
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
+ }, []]
1504
1126
  ]]
1505
1127
  ], {
1506
- click: function (evt) {
1128
+ click: function(evt) {
1507
1129
  // Capture bubble events
1508
1130
  evt.stopPropagation();
1509
1131
  }
1510
1132
  }
1511
1133
  ]);
1512
1134
 
1513
- /*------------------------*
1514
- * Add events
1515
- *------------------------*/
1516
- /*
1517
- * Button close
1518
- */
1519
- $element.querySelectorAll('.modal-close, .form-close, .delete').forEach(function (element) {
1520
- element.addEventListener('click', function(event) {
1521
- event.stopPropagation();
1522
- // TODO ver si hay algo modificado y preguntar si se desecha
1523
- $element.classList.remove('is-active');
1524
- });
1525
- });
1526
-
1527
- refresh_language($element, t);
1528
-
1529
- /*-----------------------------------------*
1530
- * Add to popup layer !!!
1531
- *-----------------------------------------*/
1532
- popup_mount_layer().appendChild($element);
1533
-
1534
- // jQuery($element).off('click').on('click', function(evt) {
1535
- // evt.stopPropagation();
1536
- // evt.preventDefault();
1537
- // });
1538
-
1539
- /*------------------------*
1540
- * Jsoneditor
1541
- *------------------------*/
1542
- $element.querySelectorAll('.jsoneditor').forEach(function (element) {
1543
- let font_family = "DejaVu Sans Mono, monospace";
1544
- let sz = 16;
1545
- element.style.setProperty('--jse-font-size-mono', sz + 'px');
1546
- element.style.setProperty('--jse-font-family-mono', font_family);
1547
-
1548
- element.jsoneditor = new JSONEditor({
1549
- target: element,
1550
- props: {
1551
- timestampTag: function({field, value, path}) {
1552
- return field === '__t__' || field === '__tm__' || field === 'tm' ||
1553
- field === 'from_t' || field === 'to_t' || field === 't' ||
1554
- field === 't_input' || field === 't_output' ||
1555
- field === 'from_tm' || field === 'to_tm' || field === 'time';
1556
- },
1557
- }
1558
- });
1559
- });
1560
-
1561
- /*---------------------------------------------------------*
1562
- * Extensions of jquery must be done after DOM attaching
1563
- *---------------------------------------------------------*/
1564
- // let $$x = jQuery($element).find('.select2-multiple');
1565
- // if($$x && $$x.select2) {
1566
- // $$x.select2();
1567
- // }
1135
+ let $body = $element.querySelector('.modal-card-body');
1568
1136
 
1569
- $element.querySelectorAll(".select2-multiple").forEach((el) => {
1570
- if(el.tomselect) {
1571
- return; // already initialized
1572
- }
1573
- new TomSelect(el, {
1574
- plugins: ["remove_button"], // Optional: Adds a remove button for multiple selections
1575
- create: false, // Set to `true` if you want users to add new options
1576
- hideSelected: true,
1577
- closeAfterSelect: false
1578
- });
1579
- });
1580
-
1581
- return $element;
1582
- }
1583
-
1584
- /************************************************************
1585
- * Show a validation error on a field
1586
- ************************************************************/
1587
- function show_field_error($field, message)
1588
- {
1589
- $field.querySelectorAll('input, textarea, select').forEach(el => {
1590
- el.classList.add('is-danger');
1591
- });
1592
- let $help = $field.querySelector('.help');
1593
- if($help) {
1594
- $help.textContent = message;
1595
- $help.style.display = 'block';
1596
- }
1597
- }
1598
-
1599
- /************************************************************
1600
- * Clear a validation error from a field
1601
- ************************************************************/
1602
- function clear_field_error($field)
1603
- {
1604
- $field.querySelectorAll('input, textarea, select').forEach(el => {
1605
- el.classList.remove('is-danger');
1606
- });
1607
- let $help = $field.querySelector('.help');
1608
- if($help) {
1609
- $help.textContent = '';
1610
- $help.style.display = 'none';
1611
- }
1612
- }
1613
-
1614
- /************************************************************
1615
- * Get the current value of a field for validation
1616
- ************************************************************/
1617
- function get_field_value_for_validation($field, conf)
1618
- {
1619
- switch(conf.tag) {
1620
- case 'input': {
1621
- let $input = $field.querySelector('input');
1622
- if(!$input) {
1623
- return '';
1624
- }
1625
- if($input.type === 'checkbox') {
1626
- return $input.checked;
1627
- }
1628
- return $input.value;
1629
- }
1630
- case 'checkbox': {
1631
- let $input = $field.querySelector('input[type="checkbox"]');
1632
- if(!$input) {
1633
- return false;
1634
- }
1635
- return $input.checked;
1636
- }
1637
- case 'textarea': {
1638
- let $el = $field.querySelector('textarea');
1639
- if(!$el) {
1640
- return '';
1641
- }
1642
- return $el.value;
1643
- }
1644
- case 'select':
1645
- case 'select2': {
1646
- let $el = $field.querySelector('select');
1647
- if(!$el) {
1648
- return '';
1649
- }
1650
- if($el.tomselect) {
1651
- return $el.tomselect.getValue();
1652
- }
1653
- return $el.value;
1654
- }
1655
- case 'jsoneditor': {
1656
- let $el = $field.querySelector('.jsoneditor');
1657
- if(!$el || !$el.jsoneditor) {
1658
- return '';
1659
- }
1660
- let val = $el.jsoneditor.get();
1661
- if(val.text !== undefined) {
1662
- return val.text;
1663
- }
1664
- return JSON.stringify(val.json);
1665
- }
1666
- case 'radio': {
1667
- let $checked = $field.querySelector('input[type="radio"]:checked');
1668
- if(!$checked) {
1669
- return '';
1670
- }
1671
- return $checked.value;
1672
- }
1673
- }
1674
- return '';
1675
- }
1676
-
1677
- /************************************************************
1678
- * Check if a field value is empty
1679
- ************************************************************/
1680
- function is_field_value_empty(value)
1681
- {
1682
- if(value === null || value === undefined || value === '') {
1683
- return true;
1684
- }
1685
- if(Array.isArray(value) && value.length === 0) {
1686
- return true;
1687
- }
1688
- return false;
1689
- }
1690
-
1691
- /************************************************************
1692
- * Determine if a field requires validation in the current mode
1693
- ************************************************************/
1694
- function is_field_required(conf, mode)
1695
- {
1696
- if(conf.is_pkey) {
1697
- // pkey is required only in create mode (unless it's a rowid)
1698
- return (mode === "create" && !conf.is_rowid);
1699
- }
1700
- if(conf.readonly) {
1701
- return false;
1702
- }
1703
- return (conf.required || conf.not_null);
1704
- }
1705
-
1706
- /************************************************************
1707
- * Validate a single field on blur
1708
- * Called from input/textarea blur event handlers
1709
- ************************************************************/
1710
- function validate_field_on_blur(element)
1711
- {
1712
- let $field = element.closest('.xfield');
1713
- if(!$field || !$field.field_conf) {
1714
- return;
1715
- }
1716
- let conf = $field.field_conf;
1717
- let $form = element.closest('form');
1718
- 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;
1719
1154
 
1720
- if(!is_field_required(conf, mode)) {
1721
- clear_field_error($field);
1722
- 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";
1723
1159
  }
1724
1160
 
1725
- let value = get_field_value_for_validation($field, conf);
1726
- if(is_field_value_empty(value)) {
1727
- show_field_error($field, t('this field is required'));
1728
- } else {
1729
- 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();
1730
1169
  }
1731
1170
  }
1732
1171
 
1733
1172
  /************************************************************
1734
- * Validate all required fields in the form
1735
- * 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).
1736
1175
  ************************************************************/
1737
- function validate_form(gobj, $form)
1176
+ function request_close_form_dialog(gobj)
1738
1177
  {
1739
- let is_valid = true;
1740
- let first_invalid = null;
1741
- let mode = $form.__mode__ || 'update';
1742
-
1743
- $form.querySelectorAll('.xfield').forEach($field => {
1744
- let conf = $field.field_conf;
1745
- if(!conf) {
1746
- return;
1747
- }
1748
-
1749
- if(!is_field_required(conf, mode)) {
1750
- 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
+ });
1751
1188
  return;
1752
1189
  }
1753
-
1754
- let value = get_field_value_for_validation($field, conf);
1755
- if(is_field_value_empty(value)) {
1756
- show_field_error($field, t('this field is required'));
1757
- is_valid = false;
1758
- if(!first_invalid) {
1759
- first_invalid = $field;
1760
- }
1761
- } else {
1762
- clear_field_error($field);
1763
- }
1764
- });
1765
-
1766
- if(first_invalid) {
1767
- let $focusable = first_invalid.querySelector('input, select, textarea');
1768
- if($focusable && $focusable.focus) {
1769
- $focusable.focus();
1770
- }
1771
1190
  }
1772
-
1773
- return is_valid;
1191
+ close_form_dialog(gobj);
1774
1192
  }
1775
1193
 
1776
1194
  /************************************************************
1777
- * Clear all validation errors from the form
1195
+ * Destroy the hosted form and remove the dialog DOM.
1778
1196
  ************************************************************/
1779
- function clear_validation(gobj, $form)
1197
+ function close_form_dialog(gobj)
1780
1198
  {
1781
- $form.querySelectorAll('.xfield').forEach($field => {
1782
- clear_field_error($field);
1783
- });
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
+ }
1784
1213
  }
1785
1214
 
1786
1215
  /************************************************************
1787
- * Before show edit/create from,
1788
- * set form state "update" or "create"
1789
- * Only the "id" field is modified,
1790
- * in "update" is readonly
1791
- * in "create" is writable
1792
- * Although by default the pkey (primary key) is "id",
1793
- * use always desc.pkey value, who knows all cases!
1216
+ * True if the topic's pkey col carries the "rowid" flag.
1794
1217
  ************************************************************/
1795
- function set_form_mode(gobj, $form, mode, index)
1218
+ function pkey_is_rowid(gobj)
1796
1219
  {
1797
1220
  let desc = gobj_read_attr(gobj, "desc");
1798
1221
  let col_pkey = kwid_find_one_record(
1799
1222
  gobj,
1800
1223
  desc.cols, // kw
1801
- null, // ids
1802
- { // jn_filter
1224
+ null, // ids
1225
+ { // jn_filter
1803
1226
  "id": desc.pkey
1804
1227
  }
1805
1228
  );
1806
- let is_rowid = str_in_list(col_pkey.flag, "rowid");
1807
-
1808
- let $id = $form.querySelector(`[name="${desc.pkey}"]`);
1809
-
1810
- if($id) {
1811
- if(mode === "update") {
1812
- /*
1813
- * update mode
1814
- */
1815
- $id.setAttribute('readonly', 'readonly');
1816
- $id.removeAttribute('required');
1817
- } else {
1818
- /*
1819
- * create mode
1820
- */
1821
- if(is_rowid) {
1822
- $id.setAttribute('readonly', 'readonly');
1823
- $id.removeAttribute('required');
1824
- } else {
1825
- $id.removeAttribute('readonly');
1826
- $id.setAttribute('required', '');
1827
- }
1828
- }
1829
- } else {
1830
- log_error(sprintf("%s: form without id field ('%s')",
1831
- gobj_short_name(gobj),
1832
- desc.pkey
1833
- ));
1834
- }
1835
-
1836
- if(is_rowid) {
1837
- mode = "create";
1229
+ if(!col_pkey) {
1230
+ return false;
1838
1231
  }
1839
-
1840
- $form.__mode__ = mode;
1841
- $form.__index__ = index;
1232
+ return str_in_list(col_pkey.flag, "rowid");
1842
1233
  }
1843
1234
 
1844
1235
  /************************************************************
@@ -1847,35 +1238,10 @@ function set_form_mode(gobj, $form, mode, index)
1847
1238
  ************************************************************/
1848
1239
  function show_edit_form(gobj, row, index)
1849
1240
  {
1850
- let popup_id = gobj_read_str_attr(gobj, "popup_id");
1851
- let form_id = gobj_read_str_attr(gobj, "form_id");
1852
-
1853
- let $element = document.getElementById(popup_id);
1854
- if(!$element) {
1855
- $element = build_topic_modal(gobj);
1856
- }
1857
-
1858
- let $form = document.getElementById(form_id);
1859
- clear_validation(gobj,$form);
1860
-
1861
- if(json_size(row) === 0 || // WARNING _check_box_state_ widely used
1862
- json_size(row) === 1 && ('_check_box_state_' in row)) {
1863
- /*
1864
- * WARNING it won't pass by here,
1865
- * "create" is call directly from Top New button
1866
- */
1867
- set_form_mode(gobj, $form, "create", index);
1868
- } else {
1869
- set_form_mode(gobj, $form, "update", index);
1870
- }
1871
-
1872
- set_form_values(gobj, $form, row);
1873
-
1874
- $element.classList.add('is-active');
1875
- let $with_focus = $element.querySelector('.with-focus');
1876
- if($with_focus) {
1877
- $with_focus.focus();
1878
- }
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);
1879
1245
  }
1880
1246
 
1881
1247
  /************************************************************
@@ -1884,24 +1250,7 @@ function show_edit_form(gobj, row, index)
1884
1250
  ************************************************************/
1885
1251
  function show_create_form(gobj, row)
1886
1252
  {
1887
- let popup_id = gobj_read_str_attr(gobj, "popup_id");
1888
- let form_id = gobj_read_str_attr(gobj, "form_id");
1889
-
1890
- let $element = document.getElementById(popup_id);
1891
- if(!$element) {
1892
- $element = build_topic_modal(gobj);
1893
- }
1894
-
1895
- let $form = document.getElementById(form_id);
1896
- clear_validation(gobj,$form);
1897
- set_form_mode(gobj, $form, "create", -1);
1898
- set_form_values(gobj, $form, row);
1899
-
1900
- $element.classList.add('is-active');
1901
- let $with_focus = $element.querySelector('.with-focus');
1902
- if($with_focus) {
1903
- $with_focus.focus();
1904
- }
1253
+ open_form_dialog(gobj, "create", row);
1905
1254
  }
1906
1255
 
1907
1256
  /************************************************************
@@ -1919,282 +1268,6 @@ function get_schema_col(gobj, id)
1919
1268
  return null;
1920
1269
  }
1921
1270
 
1922
- /************************************************************
1923
- *
1924
- ************************************************************/
1925
- function clear_data(gobj, $form)
1926
- {
1927
- $form.querySelectorAll('input, select, textarea, .jsoneditor').forEach(input => {
1928
- if(input.name !== 'id') {
1929
- if (input.classList.contains('jsoneditor')) {
1930
- let jsoneditor = input.jsoneditor;
1931
- if(jsoneditor) {
1932
- jsoneditor.set({text: ''});
1933
- }
1934
- } else if (input.type === 'checkbox') {
1935
- input.checked = false;
1936
- } else if (input.type === 'radio') {
1937
- input.checked = false;
1938
-
1939
- } else if (input.type === 'datetime-local') { /* time */
1940
- input.value = null;
1941
-
1942
- } else if (input.multiple) {
1943
- if(input.tomselect) {
1944
- input.tomselect.clear();
1945
- }
1946
- } else {
1947
- input.value = null;
1948
- }
1949
- }
1950
- });
1951
- }
1952
-
1953
- /************************************************************
1954
- *
1955
- ************************************************************/
1956
- function get_form_values($form)
1957
- {
1958
- const values = {};
1959
- // Process all form elements
1960
- $form.querySelectorAll('input, select, textarea, .jsoneditor').forEach(input => {
1961
- if (input.classList.contains('jsoneditor')) {
1962
- let jsoneditor = input.jsoneditor;
1963
- if(jsoneditor) {
1964
- let name = input.getAttribute('name');
1965
- let value = jsoneditor.get();
1966
- if(value.text===undefined) {
1967
- values[name] = JSON.stringify(value.json, null, null);
1968
- } else {
1969
- values[name] = value.text;
1970
- }
1971
- }
1972
- } else if (input.type === 'checkbox') {
1973
- // Handle single checkbox with true/false values
1974
- values[input.name] = input.checked;
1975
- } else if (input.type === 'radio') {
1976
- // Handle radio buttons
1977
- if (input.checked) {
1978
- values[input.name] = input.value;
1979
- }
1980
-
1981
- } else if (input.type === 'datetime-local') { /* time */
1982
- // Create a new Date object from the datetime-local input value
1983
- const date = new Date(input.value);
1984
-
1985
- // Convert the Date object to epoch time (in seconds)
1986
- const epochTime = Math.floor(date.getTime() / 1000);
1987
- values[input.name] = epochTime;
1988
-
1989
- } else if (input.multiple) {
1990
- // Handle multi-select with Select2
1991
- // values[input.name] = $(input).val();
1992
-
1993
- // Handle multi-select with Tom-Select
1994
- if (input.tomselect) {
1995
- values[input.name] = input.tomselect.getValue(); // Returns an array of selected values
1996
- }
1997
- } else {
1998
- // Handle other input types (text, email, number, select, textarea)
1999
- values[input.name] = input.value;
2000
- }
2001
- });
2002
-
2003
- return values;
2004
- }
2005
-
2006
- /************************************************************
2007
- * Convert a record column value from backend to frontend
2008
- ************************************************************/
2009
- function epochToDateTimeLocal(epochTime)
2010
- {
2011
- const date = new Date(epochTime * 1000); // Convert to milliseconds
2012
- const year = date.getFullYear();
2013
- const month = String(date.getMonth() + 1).padStart(2, '0');
2014
- const day = String(date.getDate()).padStart(2, '0');
2015
- const hours = String(date.getHours()).padStart(2, '0');
2016
- const minutes = String(date.getMinutes()).padStart(2, '0');
2017
- const seconds = String(date.getSeconds()).padStart(2, '0');
2018
-
2019
- // TODO for 'datetime-local' input, there is no seconds! we must use another library
2020
- return `${year}-${month}-${day}T${hours}:${minutes}`;
2021
- }
2022
-
2023
- /************************************************************
2024
- *
2025
- ************************************************************/
2026
- function set_form_values(gobj, form, values)
2027
- {
2028
- Object.keys(values).forEach(key => {
2029
- let element = form.querySelector(`[name="${key}"]`);
2030
- if (element) {
2031
- let value = values[key];
2032
- let col = get_schema_col(gobj, key);
2033
- if(col) {
2034
- value = transform__treedb_value_2_form_value(gobj, col, value);
2035
- }
2036
-
2037
- switch (element.type) {
2038
- case 'select':
2039
- element.value = value;
2040
- break;
2041
-
2042
- case 'select-one':
2043
- element.value = value;
2044
- break;
2045
-
2046
- case 'select-multiple':
2047
- // Handle multi-select with Select2
2048
- //$(element).val(value).trigger('change');
2049
-
2050
- // Handle multi-select with Tom-Select
2051
- if (element.tomselect) {
2052
- element.tomselect.setValue(value);
2053
- }
2054
- break;
2055
-
2056
- case 'radio':
2057
- // TODO review
2058
- let radio = form.querySelector(`input[name="${key}"][value="${values[key]}"]`);
2059
- if (radio) {
2060
- radio.checked = true;
2061
- }
2062
- break;
2063
-
2064
- case 'checkbox':
2065
- element.checked = value; // Assume the value is true or false
2066
- break;
2067
-
2068
- case 'text':
2069
- case 'textarea':
2070
- case 'email':
2071
- element.value = value;
2072
- break;
2073
-
2074
- case 'datetime-local': /* time */
2075
- element.value = epochToDateTimeLocal(value);
2076
- break;
2077
-
2078
- default:
2079
- if(element.classList.contains('jsoneditor')) {
2080
- let jsoneditor = element.jsoneditor;
2081
- if(jsoneditor) {
2082
- jsoneditor.set(value);
2083
- }
2084
- break;
2085
- }
2086
-
2087
- element.value = value;
2088
- log_error(
2089
- sprintf("%s: form type unknown: '%s'",
2090
- gobj_short_name(gobj),
2091
- element.type
2092
- )
2093
- );
2094
- break;
2095
- }
2096
- }
2097
- });
2098
- }
2099
-
2100
- /************************************************************
2101
- * Convert a record column value from backend
2102
- * to frontend form field
2103
- ************************************************************/
2104
- function transform__treedb_value_2_form_value(gobj, col, value)
2105
- {
2106
- const field_desc = treedb_get_field_desc(col);
2107
-
2108
- switch(field_desc.type) {
2109
- case "string":
2110
- case "email": // string subtype
2111
- case "tel": // string subtype
2112
- case "url": // string subtype
2113
- break;
2114
- case "integer":
2115
- break;
2116
- case "real":
2117
- break;
2118
- case "boolean":
2119
- break;
2120
-
2121
- case "object":
2122
- case "dict":
2123
- case "template":
2124
- case "array":
2125
- case "list":
2126
- case "coordinates":
2127
- case "blob":
2128
- // Return as it is, manage by jsoneditor
2129
- value = {json: value};
2130
- // value = {text: JSON.stringify(value)}; // other way
2131
- break;
2132
-
2133
- case "enum":
2134
- switch(field_desc.real_type) {
2135
- case "string":
2136
- break;
2137
- case "object":
2138
- case "dict":
2139
- case "array":
2140
- case "list":
2141
- break;
2142
- }
2143
- break;
2144
-
2145
- case "now":
2146
- case "time":
2147
- switch(field_desc.real_type) {
2148
- case "string":
2149
- case "integer":
2150
- value = parseInt(value) || 0;
2151
- value = new Date(value);
2152
- break;
2153
- }
2154
- break;
2155
-
2156
- case "color":
2157
- switch(field_desc.real_type) {
2158
- case "string":
2159
- // TODO
2160
- break;
2161
- case "integer":
2162
- // TODO
2163
- break;
2164
- }
2165
- break;
2166
-
2167
- case "hook": // Convert data from backend to frontend FORM FIELD
2168
- // WARNING hook fields are not writable and by now they are not showed in forms.
2169
- break;
2170
-
2171
- case "fkey": // Convert data from backend to frontend FORM FIELD
2172
- let new_value = [];
2173
- if(value) {
2174
- if(is_string(value)) {
2175
- let fkey = treedb_decoder_fkey(col, value);
2176
- if(fkey) {
2177
- new_value.push(fkey.id);
2178
- }
2179
- } else if(is_array(value)) {
2180
- for(let i=0; i<value.length; i++) {
2181
- let fkey = treedb_decoder_fkey(col, value[i]);
2182
- if(fkey) {
2183
- new_value.push(fkey.id);
2184
- }
2185
- }
2186
- } else {
2187
- log_error("fkey type unsupported: " + JSON.stringify(value));
2188
- }
2189
- }
2190
-
2191
- value = new_value;
2192
- break;
2193
- }
2194
-
2195
- return value;
2196
- }
2197
-
2198
1271
  /************************************************************
2199
1272
  * Convert from frontend to backend
2200
1273
  * operation: "create" "update"
@@ -2374,24 +1447,6 @@ function transform__form_value_2_treedb_value(gobj, col, value, operation)
2374
1447
  return value;
2375
1448
  }
2376
1449
 
2377
- /************************************************************
2378
- *
2379
- ************************************************************/
2380
- function get_fkey_options(gobj, col)
2381
- {
2382
- for(let topic_name of Object.keys(col.fkey)) {
2383
- // HACK we work only with one fkey
2384
- let kw = {
2385
- topic_name: topic_name
2386
- };
2387
- let webix = gobj_command(gobj_parent(gobj), "get_topic_data", kw, gobj);
2388
- let current_list = webix.data;
2389
- return list2options(current_list, "id", "id");
2390
- }
2391
- log_error("No fkey options found" + JSON.stringify(col));
2392
- return null;
2393
- }
2394
-
2395
1450
  /************************************************************
2396
1451
  *
2397
1452
  ************************************************************/
@@ -2931,58 +1986,29 @@ function ac_paste_rows(gobj, event, kw, src)
2931
1986
  }
2932
1987
 
2933
1988
  /************************************************************
2934
- * 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.
2935
1992
  ************************************************************/
2936
- function ac_create_record(gobj, event, kw, src)
1993
+ function ac_form_save_record(gobj, event, kw, src)
2937
1994
  {
2938
- let new_kw = null;
2939
- try {
2940
- new_kw = transform__form_record_2_treedb_record(gobj, kw, "create");
2941
- } catch (e) {
2942
- log_warning(e);
2943
- throw e;
2944
- }
2945
-
1995
+ let mode = gobj_read_str_attr(src, "form_mode");
2946
1996
  gobj_publish_event(
2947
1997
  gobj,
2948
- "EV_CREATE_RECORD",
1998
+ (mode === "create")? "EV_CREATE_RECORD" : "EV_UPDATE_RECORD",
2949
1999
  {
2950
2000
  topic_name: gobj_read_str_attr(gobj, "topic_name"),
2951
- record: new_kw
2001
+ record: kw
2952
2002
  }
2953
2003
  );
2954
2004
 
2955
- /*
2956
- * Return -1 will not close the form
2957
- */
2958
- return 0;
2959
- }
2960
-
2961
- /************************************************************
2962
- * From internal Save button of "update" modal form
2963
- ************************************************************/
2964
- function ac_update_record(gobj, event, kw, src)
2965
- {
2966
- let new_kw = null;
2967
- try {
2968
- new_kw = transform__form_record_2_treedb_record(gobj, kw, "update");
2969
- } catch (e) {
2970
- log_warning(e);
2971
- throw e;
2972
- }
2973
-
2974
- gobj_publish_event(
2975
- gobj,
2976
- "EV_UPDATE_RECORD",
2977
- {
2978
- topic_name: gobj_read_str_attr(gobj, "topic_name"),
2979
- record: new_kw
2980
- }
2981
- );
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);
2982
2011
 
2983
- /*
2984
- * Return -1 will not close the form
2985
- */
2986
2012
  return 0;
2987
2013
  }
2988
2014
 
@@ -3161,8 +2187,7 @@ function create_gclass(gclass_name)
3161
2187
  ["EV_NODE_DELETED", ac_node_deleted, null],
3162
2188
 
3163
2189
  ["EV_EDITION_MODE", ac_edition_mode, null],
3164
- ["EV_CREATE_RECORD", ac_create_record, null],
3165
- ["EV_UPDATE_RECORD", ac_update_record, null],
2190
+ ["EV_SAVE_RECORD", ac_form_save_record, null],
3166
2191
 
3167
2192
  ["EV_NEW_ROW", ac_new_row, null],
3168
2193
  ["EV_DELETE_ROWS", ac_delete_rows, null],
@@ -3188,6 +2213,7 @@ function create_gclass(gclass_name)
3188
2213
  ["EV_NODE_DELETED", 0],
3189
2214
 
3190
2215
  ["EV_EDITION_MODE", 0],
2216
+ ["EV_SAVE_RECORD", 0],
3191
2217
  ["EV_NEW_ROW", 0],
3192
2218
  ["EV_DELETE_ROWS", 0],
3193
2219
  ["EV_COPY_ROWS", 0],
@@ -3233,6 +2259,11 @@ function create_gclass(gclass_name)
3233
2259
  ***************************************************************/
3234
2260
  function register_c_yui_treedb_topic_with_form()
3235
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
+ }
3236
2267
  return create_gclass(GCLASS_NAME);
3237
2268
  }
3238
2269