@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.
package/src/c_yui_form.js CHANGED
@@ -45,12 +45,17 @@ import {
45
45
  gobj_read_str_attr,
46
46
  gobj_publish_event,
47
47
  refresh_language,
48
+ treedb_decoder_fkey,
49
+ gclass_find_by_name,
48
50
  } from "@yuneta/gobj-js";
49
51
 
50
52
  import {t} from "i18next";
51
53
  import "tom-select/dist/css/tom-select.css"; // Import Tom-Select CSS
52
54
  import TomSelect from "tom-select"; // Import Tom-Select JS
53
55
 
56
+ import { JSONEditor } from 'vanilla-jsoneditor';
57
+ import "vanilla-jsoneditor/themes/jse-theme-dark.css";
58
+
54
59
  import "tabulator-tables/dist/css/tabulator.min.css"; // Import Tabulator CSS
55
60
  import "tabulator-tables/dist/css/tabulator_bulma.css";
56
61
  import { TabulatorFull as Tabulator } from "tabulator-tables"; // Import Full Tabulator JS
@@ -74,6 +79,10 @@ SDATA(data_type_t.DTP_POINTER, "$parent", 0, null, "$container will
74
79
  SDATA(data_type_t.DTP_POINTER, "template", 0, "<div>You must supply a template for form</div>", "Template for form, object -> TEMPLATE, array -> DESC"),
75
80
  SDATA(data_type_t.DTP_JSON, "record", 0, "{}", "Data to form"),
76
81
  SDATA(data_type_t.DTP_JSON, "placeholders", 0, "{}", "Placeholder values for form fields"),
82
+ SDATA(data_type_t.DTP_JSON, "fkey_options", 0, "{}", "Options for fkey fields: {topic_name: [ids or {id} records]}, read at build time"),
83
+ SDATA(data_type_t.DTP_STRING, "form_mode", 0, "", "'create' (pkey editable+required) or 'update' (pkey readonly); empty = no pkey handling"),
84
+ SDATA(data_type_t.DTP_STRING, "pkey", 0, "id", "Primary key field name, used by form_mode"),
85
+ SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "Treedb topic name: labels use the i18n cascade '<topic>.<col>' -> '<col>' -> header (like table headers)"),
77
86
  SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Default is editable, set false to readonly"),
78
87
  SDATA(data_type_t.DTP_BOOLEAN, "selectable", 0, true, "Rows selectable with 'click' (BAD with editable=true)"),
79
88
  SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, false, "Show a first column with checkbox to select rows"),
@@ -284,31 +293,31 @@ function build_ui(gobj)
284
293
  *
285
294
  *----------------------------------------------*/
286
295
  let $container = createElement2(
287
- ['div', {class: 'C_YUI_FORM is-flex is-flex-direction-row overscroll-contain', style:'height:100%; width:100%; box-sizing:border-box;'}, [
296
+ ['div', {class: 'C_YUI_FORM is-flex is-flex-direction-column overscroll-contain', style:'height:100%; width:100%; box-sizing:border-box;'}, [
288
297
 
289
298
  /*----------------------------*
290
299
  * Form
291
300
  *----------------------------*/
292
301
  ['div', {
293
302
  class: 'yui-form is-flex-grow-1 p-2',
294
- style: 'overflow-y:auto; height:100%;min-height:0;'
303
+ style: 'overflow-y:auto; min-height:0;'
295
304
  }, build_form(gobj)],
296
305
 
297
306
  /*----------------------------*
298
- * Right toolbar
307
+ * Bottom toolbar
299
308
  *----------------------------*/
300
- ['div', {class: 'yui-toolbar-form ml-2 is-flex-shrink-0 is-flex is-flex-direction-column is-justify-content-space-between', style: 'overflow-y:auto; height:100%; max-width:90px;'}, [
309
+ ['div', {class: 'yui-toolbar-form is-flex-shrink-0 is-flex is-flex-direction-row is-justify-content-space-between is-align-items-center', style: 'width:100%; flex-wrap:wrap; row-gap:6px;'}, [
301
310
 
302
311
  /*----------------------------*
303
- * Top buttons
312
+ * Left buttons
304
313
  *----------------------------*/
305
- ['div', {class: 'p-1 is-flex is-flex-direction-column is-align-items-center', style:'width:100%; row-gap:6px;'}, [
314
+ ['div', {class: 'p-1 is-flex is-flex-direction-row is-align-items-center', style:'column-gap:6px;'}, [
306
315
  /*----------------------------*
307
316
  * Save
308
317
  *----------------------------*/
309
- ['button', {class: 'button p-1 is-flex is-flex-direction-column is-align-items-center button-save', style:'width:100%;', disabled: true}, [
318
+ ['button', {class: 'button p-1 button-save', title: 'save', 'aria-label': 'save', disabled: true}, [
310
319
  ['span', {class: 'icon m-0'}, '<i class="yi-floppy-disk"></i>'],
311
- ['span', {class: 'is-hidden-mobile', i18n: 'save'}, 'save']
320
+ ['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'save'}, 'save']
312
321
  ], {
313
322
  click: function(evt) {
314
323
  evt.stopPropagation();
@@ -319,9 +328,9 @@ function build_ui(gobj)
319
328
  /*----------------------------*
320
329
  * Undo
321
330
  *----------------------------*/
322
- ['button', {class: 'button p-1 is-flex is-flex-direction-column is-align-items-center button-undo', style:'width:100%;', disabled: true}, [
331
+ ['button', {class: 'button p-1 button-undo', title: 'undo', 'aria-label': 'undo', disabled: true}, [
323
332
  ['span', {class: 'icon m-0'}, '<i class="yi-arrow-rotate-left"></i>'],
324
- ['span', {class: 'is-hidden-mobile', i18n: 'undo'}, 'undo']
333
+ ['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'undo'}, 'undo']
325
334
  ], {
326
335
  click: function(evt) {
327
336
  evt.stopPropagation();
@@ -332,9 +341,9 @@ function build_ui(gobj)
332
341
  /*----------------------------*
333
342
  * Clear
334
343
  *----------------------------*/
335
- ['button', {class: 'button p-1 is-flex is-flex-direction-column is-align-items-center', style:'width:100%;'}, [
344
+ ['button', {class: 'button p-1', title: 'clear', 'aria-label': 'clear'}, [
336
345
  ['span', {class: 'icon m-0'}, '<i class="yi-broom-wide"></i>'],
337
- ['span', {class: 'is-hidden-mobile', i18n: 'clear'}, 'clear']
346
+ ['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'clear'}, 'clear']
338
347
  ], {
339
348
  click: function(evt) {
340
349
  evt.stopPropagation();
@@ -344,15 +353,15 @@ function build_ui(gobj)
344
353
  ]],
345
354
 
346
355
  /*----------------------------*
347
- * Bottom buttons
356
+ * Right buttons
348
357
  *----------------------------*/
349
- ['div', {class: 'p-1 is-flex is-flex-direction-column is-align-items-center', style:'width:100%; row-gap:6px;'}, [
358
+ ['div', {class: 'p-1 is-flex is-flex-direction-row is-align-items-center', style:'column-gap:6px;'}, [
350
359
  /*----------------------------*
351
360
  * Copy
352
361
  *----------------------------*/
353
- ['button', {class: 'button p-1 is-flex is-flex-direction-column is-align-items-center', style:'width:100%;'}, [
362
+ ['button', {class: 'button p-1', title: 'copy', 'aria-label': 'copy'}, [
354
363
  ['span', {class: 'icon m-0'}, '<i class="yi-copy"></i>'],
355
- ['span', {class: 'is-hidden-mobile', i18n: 'copy'}, 'copy']
364
+ ['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'copy'}, 'copy']
356
365
  ], {click: function(evt) {
357
366
  evt.stopPropagation();
358
367
  gobj_send_event(gobj, "EV_COPY_RECORD", {}, gobj);
@@ -362,9 +371,9 @@ function build_ui(gobj)
362
371
  /*----------------------------*
363
372
  * Paste
364
373
  *----------------------------*/
365
- ['button', {class: 'button p-1 is-flex is-flex-direction-column is-align-items-center', style:'width:100%;'}, [
374
+ ['button', {class: 'button p-1', title: 'paste', 'aria-label': 'paste'}, [
366
375
  ['span', {class: 'icon m-0'}, '<i class="yi-paste"></i>'],
367
- ['span', {class: 'is-hidden-mobile', i18n: 'paste'}, 'paste']
376
+ ['span', {class: 'is-hidden-mobile pl-1 pr-1', i18n: 'paste'}, 'paste']
368
377
  ], {click: async function(evt) {
369
378
  evt.stopPropagation();
370
379
  await readClipboard(gobj);
@@ -376,6 +385,7 @@ function build_ui(gobj)
376
385
  );
377
386
 
378
387
  gobj_write_attr(gobj, "$container", $container);
388
+ apply_form_mode(gobj, $container.querySelector('form'));
379
389
  refresh_language($container, t);
380
390
 
381
391
  let $parent = gobj_read_attr(gobj, "$parent");
@@ -446,7 +456,25 @@ function build_html_form(gobj, $form, prefix, template)
446
456
  * items not filled
447
457
  */
448
458
  html_field_conf.name = field_desc.name;
449
- html_field_conf.label = field_desc.header;
459
+ /*
460
+ * Label i18n: mirror the table-header cascade (col_label) so a
461
+ * column translates the same in the form and the table. The
462
+ * visible text resolves '<topic>.<col>' -> '<col>' -> header at
463
+ * build. Only set `label_i18n` (the retranslation key) when a
464
+ * translation actually exists, so refresh_language never
465
+ * clobbers an untranslated label with the raw col id — it keeps
466
+ * the header, exactly like col_label's `defaultValue`.
467
+ */
468
+ let topic_name = gobj_read_str_attr(gobj, "topic_name");
469
+ let label_keys = topic_name
470
+ ? [topic_name + "." + field_desc.name, field_desc.name]
471
+ : [field_desc.name];
472
+ const NO_I18N = "\x00";
473
+ html_field_conf.label = t(label_keys, {defaultValue: field_desc.header});
474
+ html_field_conf.label_i18n =
475
+ (t(field_desc.name, {defaultValue: NO_I18N}) !== NO_I18N)
476
+ ? field_desc.name
477
+ : null;
450
478
  html_field_conf.placeholder = field_desc.placeholder?t(field_desc.placeholder):'';
451
479
 
452
480
  let placeholders = gobj_read_attr(gobj, "placeholders");
@@ -502,12 +530,13 @@ function build_form_field_conf(gobj, field_desc)
502
530
  switch(field_desc.type) {
503
531
  case "object":
504
532
  case "dict":
505
- field_conf.tag = "fieldset";
533
+ /* free-form dict (no subschema mechanism) edits as raw
534
+ * JSON; structured dicts use the "template" flag */
535
+ field_conf.tag = "jsoneditor";
506
536
  break;
507
537
 
508
538
  case "template":
509
539
  field_conf.tag = "fieldset";
510
- // TODO si es para editar, tiene que ser field_conf.tag = "jsoneditor";
511
540
  field_conf.options = field_desc.enum_list;
512
541
  break;
513
542
 
@@ -522,7 +551,9 @@ function build_form_field_conf(gobj, field_desc)
522
551
 
523
552
  case "array":
524
553
  case "list":
525
- field_conf.tag = "table";
554
+ /* free-form list (no subschema) edits as raw JSON;
555
+ * structured lists use the "table" flag */
556
+ field_conf.tag = "jsoneditor";
526
557
  break;
527
558
 
528
559
  case "enum":
@@ -546,7 +577,8 @@ function build_form_field_conf(gobj, field_desc)
546
577
  break;
547
578
 
548
579
  case "fkey":
549
- // build later
580
+ field_conf.tag = "select2";
581
+ field_conf.options = get_fkey_select_options(gobj, field_desc);
550
582
  break;
551
583
 
552
584
  case "image":
@@ -660,6 +692,149 @@ function build_form_field_conf(gobj, field_desc)
660
692
  return field_conf;
661
693
  }
662
694
 
695
+ /************************************************************
696
+ * Options for a fkey field: the ids of the parent topic's
697
+ * rows, taken from the `fkey_options` attr
698
+ * ({topic_name: [ids or {id} records]}, supplied by the host —
699
+ * the form never queries the backend or its parent itself).
700
+ * Read at build time.
701
+ ************************************************************/
702
+ function get_fkey_select_options(gobj, field_desc)
703
+ {
704
+ let fkey_desc = field_desc.fkey;
705
+ if(!is_object(fkey_desc) || Object.keys(fkey_desc).length === 0) {
706
+ log_error(sprintf("%s: fkey field '%s' without fkey mapping",
707
+ gobj_short_name(gobj), field_desc.name
708
+ ));
709
+ return [];
710
+ }
711
+
712
+ // HACK we work only with one fkey (same as the treedb stack)
713
+ let topic_name = Object.keys(fkey_desc)[0];
714
+
715
+ let fkey_options = gobj_read_attr(gobj, "fkey_options");
716
+ let list = is_object(fkey_options)? fkey_options[topic_name] : null;
717
+ if(!is_array(list)) {
718
+ log_warning(sprintf("%s: no fkey_options for topic '%s' (field '%s')",
719
+ gobj_short_name(gobj), topic_name, field_desc.name
720
+ ));
721
+ return [];
722
+ }
723
+
724
+ let options = [];
725
+ for(let item of list) {
726
+ let id = is_object(item)? item.id : item;
727
+ if(is_string(id) && !empty_string(id)) {
728
+ options.push(id);
729
+ }
730
+ }
731
+ return options;
732
+ }
733
+
734
+ /************************************************************
735
+ * Encode the select2 value (array of ids) into fkey ref(s)
736
+ * "topic_name^id^hook_name", shaped by the column real type:
737
+ * string -> single ref (or null if no selection)
738
+ * object/dict -> {ref: true, ...}
739
+ * array/list -> [ref, ...]
740
+ ************************************************************/
741
+ function build_fkey_ref(gobj, field_desc, value)
742
+ {
743
+ let fkey_desc = field_desc.fkey;
744
+ if(!is_object(fkey_desc) || Object.keys(fkey_desc).length === 0) {
745
+ log_error(sprintf("%s: fkey field '%s' without fkey mapping",
746
+ gobj_short_name(gobj), field_desc.name
747
+ ));
748
+ return null;
749
+ }
750
+
751
+ // HACK we work only with one fkey (same as the treedb stack)
752
+ let topic_name = Object.keys(fkey_desc)[0];
753
+ let hook_name = fkey_desc[topic_name];
754
+
755
+ let refs;
756
+ switch(field_desc.real_type) {
757
+ case "string":
758
+ refs = null;
759
+ break;
760
+ case "object":
761
+ case "dict":
762
+ refs = {};
763
+ break;
764
+ case "array":
765
+ case "list":
766
+ refs = [];
767
+ break;
768
+ default:
769
+ log_error(sprintf("%s: fkey field '%s' with bad real_type '%s'",
770
+ gobj_short_name(gobj), field_desc.name, field_desc.real_type
771
+ ));
772
+ return null;
773
+ }
774
+
775
+ if(!is_array(value)) {
776
+ value = value? [value] : [];
777
+ }
778
+
779
+ for(let v of value) {
780
+ let id = is_object(v)? v.id : v;
781
+ if(!is_string(id) || empty_string(id)) {
782
+ log_error(sprintf("%s: fkey field '%s' with bad value: %s",
783
+ gobj_short_name(gobj), field_desc.name, JSON.stringify(v)
784
+ ));
785
+ continue;
786
+ }
787
+ let ref = topic_name + "^" + id + "^" + hook_name;
788
+ switch(field_desc.real_type) {
789
+ case "string":
790
+ return ref; // single-valued: first id wins
791
+ case "object":
792
+ case "dict":
793
+ refs[ref] = true;
794
+ break;
795
+ case "array":
796
+ case "list":
797
+ refs.push(ref);
798
+ break;
799
+ }
800
+ }
801
+
802
+ return refs;
803
+ }
804
+
805
+ /************************************************************
806
+ * Apply the form mode to the pkey field:
807
+ * "update" -> pkey readonly, not required
808
+ * "create" -> pkey writable and required (readonly if rowid)
809
+ * Empty form_mode keeps the template-declared behaviour
810
+ * (backward compatible).
811
+ ************************************************************/
812
+ function apply_form_mode(gobj, $form)
813
+ {
814
+ let mode = gobj_read_str_attr(gobj, "form_mode");
815
+ if(empty_string(mode)) {
816
+ return;
817
+ }
818
+
819
+ let pkey = gobj_read_str_attr(gobj, "pkey");
820
+ let $input = $form.querySelector(`[name="${pkey}"]`);
821
+ if(!$input) {
822
+ log_error(sprintf("%s: form_mode '%s' but form has no pkey field '%s'",
823
+ gobj_short_name(gobj), mode, pkey
824
+ ));
825
+ return;
826
+ }
827
+
828
+ let is_rowid = $input.field_desc && $input.field_desc.type === "rowid";
829
+ if(mode === "create" && !is_rowid) {
830
+ $input.removeAttribute("readonly");
831
+ $input.setAttribute("required", "");
832
+ } else {
833
+ $input.setAttribute("readonly", "readonly");
834
+ $input.removeAttribute("required");
835
+ }
836
+ }
837
+
663
838
  /******************************************************************
664
839
  * With html field conf builds an html element,
665
840
  * ready to add to the form
@@ -700,6 +875,7 @@ function create_form_field(
700
875
  inputType,
701
876
  name,
702
877
  label,
878
+ label_i18n,
703
879
  placeholder,
704
880
  options,
705
881
  extras, // extra html input/textarea attributes
@@ -750,7 +926,7 @@ function create_form_field(
750
926
  $element = createElement2(
751
927
  ['div', {class: 'xfield field is-horizontal'}, [
752
928
  ['div', {class: 'field-label is-normal' }, [
753
- ['label', {class:'label ', i18n:label, for:name}, label]
929
+ ["label", {class:"label ", i18n:(label_i18n||label), for:name}, label]
754
930
  ]],
755
931
 
756
932
  ['div', {class: 'field-body'}, [
@@ -765,7 +941,7 @@ function create_form_field(
765
941
  $element = createElement2(
766
942
  ['div', {class: 'xfield field is-horizontal'}, [
767
943
  ['div', {class: 'field-label is-normal' }, [
768
- ['label', {class:'label ', i18n:label, for:name}, label]
944
+ ["label", {class:"label ", i18n:(label_i18n||label), for:name}, label]
769
945
  ]],
770
946
 
771
947
  ['div', {class: 'field-body'}, [
@@ -788,7 +964,7 @@ function create_form_field(
788
964
  $element = createElement2(
789
965
  ['div', {class: 'xfield field is-horizontal'}, [
790
966
  ['div', {class: 'field-label is-normal' }, [
791
- ['label', {class:'label ', i18n:label, for:name}, label]
967
+ ["label", {class:"label ", i18n:(label_i18n||label), for:name}, label]
792
968
  ]],
793
969
 
794
970
  ['div', {class: 'field-body'}, [
@@ -804,7 +980,7 @@ function create_form_field(
804
980
  $element = createElement2(
805
981
  ['div', {class: 'xfield field is-horizontal'}, [
806
982
  ['div', {class: 'field-label is-normal' }, [
807
- ['label', {class:'label ', i18n:label, for:name}, label]
983
+ ["label", {class:"label ", i18n:(label_i18n||label), for:name}, label]
808
984
  ]],
809
985
 
810
986
  ['div', {class: 'field-body'}, [
@@ -823,7 +999,7 @@ function create_form_field(
823
999
  $element = createElement2(
824
1000
  ['div', {class: 'field is-horizontal tabulator-buttons'}, [
825
1001
  ['div', {class: 'field-label is-normal' }, [
826
- ['label', {class:'label', i18n:label}, label]
1002
+ ['label', {class:'label', i18n:(label_i18n||label)}, label]
827
1003
  ]],
828
1004
 
829
1005
  ['div', {class: 'field-body'}, [
@@ -866,7 +1042,7 @@ function create_form_field(
866
1042
  $element = createElement2(
867
1043
  ['div', {class: 'xfield field is-horizontal'}, [
868
1044
  ['div', {class: 'field-label is-normal' }, [
869
- ['label', {class:'label ', i18n:label, for:name}, label]
1045
+ ["label", {class:"label ", i18n:(label_i18n||label), for:name}, label]
870
1046
  ]],
871
1047
 
872
1048
  ['div', {class: 'field-body'}, [
@@ -895,7 +1071,7 @@ function create_form_field(
895
1071
  case 'input':
896
1072
  {
897
1073
  let attrs = {
898
- class: `input ${name==='id'?'with-focus':''}`,
1074
+ class: `input ${name===gobj_read_str_attr(gobj, "pkey")?'with-focus':''}`,
899
1075
  name: name,
900
1076
  type: inputType,
901
1077
  placeholder: placeholder
@@ -968,11 +1144,19 @@ function create_form_field(
968
1144
  },
969
1145
  options.map(option =>
970
1146
  ['option', {value:option, i18n:option}, option]
971
- )
1147
+ ),
1148
+ {
1149
+ 'change': function (evt) {
1150
+ gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
1151
+ }
1152
+ }
972
1153
  ]
973
1154
  ];
974
- $extend = createElement2(extend);
975
- $control.appendChild($extend);
1155
+ let $wrapper = createElement2(extend);
1156
+ $control.appendChild($wrapper);
1157
+ /* the data-input marker must land on the SELECT, not on the
1158
+ * bulma .select wrapper — get/set read/write $input.value */
1159
+ $extend = $wrapper.querySelector('select');
976
1160
  break;
977
1161
  }
978
1162
 
@@ -996,6 +1180,9 @@ function create_form_field(
996
1180
  $extend.tom_select = new TomSelect($extend, {
997
1181
  allowEmptyOption: true,
998
1182
  placeholder: placeholder,
1183
+ /* single-valued fkey (real_type string): one selection only */
1184
+ maxItems: (field_desc.type === "fkey" &&
1185
+ field_desc.real_type === "string")? 1 : null,
999
1186
  plugins: {
1000
1187
  'clear_button': {
1001
1188
  'title': t('Remove all selected options'),
@@ -1014,10 +1201,17 @@ function create_form_field(
1014
1201
  case 'checkbox':
1015
1202
  {
1016
1203
  let extend = ['label', {class: 'checkbox'},
1017
- ['input', {type: 'checkbox', name:name}]
1204
+ ['input', {type: 'checkbox', name:name}, '', {
1205
+ 'change': function (evt) {
1206
+ gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
1207
+ }
1208
+ }]
1018
1209
  ];
1019
- $extend = createElement2(extend);
1020
- $control.appendChild($extend);
1210
+ let $wrapper = createElement2(extend);
1211
+ $control.appendChild($wrapper);
1212
+ /* the data-input marker must land on the checkbox INPUT,
1213
+ * not on the label — get/set read/write $input.checked */
1214
+ $extend = $wrapper.querySelector('input');
1021
1215
  break;
1022
1216
  }
1023
1217
 
@@ -1043,6 +1237,7 @@ function create_form_field(
1043
1237
  {
1044
1238
  let attrs = {
1045
1239
  class: 'jsoneditor jse-theme-dark',
1240
+ name: name,
1046
1241
  style: ''
1047
1242
  };
1048
1243
  Object.assign(attrs, extras);
@@ -1055,6 +1250,22 @@ function create_form_field(
1055
1250
  let extend = ['div', attrs];
1056
1251
  $extend = createElement2(extend);
1057
1252
  $control.appendChild($extend);
1253
+ $extend.jsoneditor = new JSONEditor({
1254
+ target: $extend,
1255
+ props: {
1256
+ readOnly: !!readonly,
1257
+ onChange: function() {
1258
+ gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
1259
+ },
1260
+ timestampTag: function({field, value, path}) {
1261
+ return field === '__t__' || field === '__tm__' ||
1262
+ field === 'tm' || field === 't' || field === 'time' ||
1263
+ field === 'from_t' || field === 'to_t' ||
1264
+ field === 't_input' || field === 't_output' ||
1265
+ field === 'from_tm' || field === 'to_tm';
1266
+ },
1267
+ }
1268
+ });
1058
1269
  break;
1059
1270
  }
1060
1271
 
@@ -1828,8 +2039,9 @@ function get_form_values(gobj, $form)
1828
2039
  ************************************************************/
1829
2040
  function clear_data(gobj, $form)
1830
2041
  {
2042
+ let pkey = gobj_read_str_attr(gobj, "pkey");
1831
2043
  $form.querySelectorAll('input, select, textarea, .jsoneditor').forEach($input => {
1832
- if($input.name !== 'id') { // TODO review
2044
+ if($input.name !== pkey) {
1833
2045
  if($input.classList.contains("jsoneditor")) {
1834
2046
  let jsoneditor = $input.jsoneditor;
1835
2047
  if(jsoneditor) {
@@ -1923,7 +2135,7 @@ function set_form_values(gobj, template, $form, record)
1923
2135
 
1924
2136
  clear_data(gobj, $form);
1925
2137
 
1926
- if(!record || record.length === 0) {
2138
+ if(!record || Object.keys(record).length === 0) {
1927
2139
  record = create_template_record(template);
1928
2140
  }
1929
2141
 
@@ -1948,12 +2160,28 @@ function set_form_values(gobj, template, $form, record)
1948
2160
  modified = true;
1949
2161
  }
1950
2162
  value = treedb_value_2_form_value(gobj, field_desc, value);
2163
+ if(value === undefined) {
2164
+ /* DOM 'value' setters stringify undefined as "undefined";
2165
+ * null is safe (LegacyNullToEmptyString). */
2166
+ value = null;
2167
+ }
1951
2168
 
1952
2169
  switch(tag) {
1953
2170
  case "input":
1954
2171
  switch(input_type) {
1955
2172
  case "datetime-local":
1956
- // TODO $input.value = epochToDateTimeLocal(value);
2173
+ /* value is a Date (see treedb_value_2_form_value);
2174
+ * a datetime-local input wants local
2175
+ * "YYYY-MM-DDTHH:mm" (no seconds). */
2176
+ if(value instanceof Date && !isNaN(value.getTime())) {
2177
+ let pad = (n) => String(n).padStart(2, "0");
2178
+ $input.value =
2179
+ `${value.getFullYear()}-${pad(value.getMonth() + 1)}` +
2180
+ `-${pad(value.getDate())}T` +
2181
+ `${pad(value.getHours())}:${pad(value.getMinutes())}`;
2182
+ } else {
2183
+ $input.value = "";
2184
+ }
1957
2185
  break;
1958
2186
  case "color":
1959
2187
  $input.value = convertColor(value);
@@ -1988,7 +2216,7 @@ function set_form_values(gobj, template, $form, record)
1988
2216
  case "jsoneditor":
1989
2217
  let jsoneditor = $input.jsoneditor;
1990
2218
  if(jsoneditor) {
1991
- jsoneditor.set(value);
2219
+ jsoneditor.set(value ? value : {text: ''});
1992
2220
  }
1993
2221
  break;
1994
2222
 
@@ -2000,7 +2228,7 @@ function set_form_values(gobj, template, $form, record)
2000
2228
  break;
2001
2229
 
2002
2230
  case "table":
2003
- load_tabulator_data(gobj, $input, value);
2231
+ load_tabulator_data(gobj, $input, value ? value : []);
2004
2232
  break;
2005
2233
 
2006
2234
  case "coordinates":
@@ -2080,7 +2308,35 @@ function treedb_value_2_form_value(gobj, field_desc, value)
2080
2308
  let real_type = field_desc.real_type;
2081
2309
 
2082
2310
  switch(type) {
2083
- case "fkey":
2311
+ case "fkey": {
2312
+ /* Decode ref(s) ("topic^id^hook", bare "$id", dict keys or
2313
+ * {topic_name,id,hook_name}) into a plain array of ids for
2314
+ * the select2 widget. */
2315
+ let new_value = [];
2316
+ if(is_string(value) && !empty_string(value)) {
2317
+ let fkey = treedb_decoder_fkey(field_desc, value);
2318
+ if(fkey) {
2319
+ new_value.push(fkey.id);
2320
+ }
2321
+ } else if(is_array(value)) {
2322
+ for(let v of value) {
2323
+ let fkey = treedb_decoder_fkey(field_desc, v);
2324
+ if(fkey) {
2325
+ new_value.push(fkey.id);
2326
+ }
2327
+ }
2328
+ } else if(is_object(value)) {
2329
+ // dict-type fkeys store refs as keys {"topic^id^hook": true}
2330
+ for(let v of Object.keys(value)) {
2331
+ let fkey = treedb_decoder_fkey(field_desc, v);
2332
+ if(fkey) {
2333
+ new_value.push(fkey.id);
2334
+ }
2335
+ }
2336
+ }
2337
+ value = new_value;
2338
+ break;
2339
+ }
2084
2340
  case "hook":
2085
2341
  case "enum":
2086
2342
  case "uuid":
@@ -2091,8 +2347,9 @@ function treedb_value_2_form_value(gobj, field_desc, value)
2091
2347
  break;
2092
2348
  case "now":
2093
2349
  case "time":
2094
- value = parseInt(value) || 0;
2095
- value = new Date(value);
2350
+ /* Stored epoch is in SECONDS (Yuneta/timeranger convention),
2351
+ * matching get_form_values() which saves getTime()/1000. */
2352
+ value = new Date((parseInt(value) || 0) * 1000);
2096
2353
  break;
2097
2354
  case "date":
2098
2355
  case "color":
@@ -2110,17 +2367,22 @@ function treedb_value_2_form_value(gobj, field_desc, value)
2110
2367
 
2111
2368
  case "string":
2112
2369
  case "integer":
2113
- case "object":
2114
- case "dict":
2115
- case "array":
2116
- case "list":
2117
2370
  case "real":
2118
2371
  case "boolean":
2119
2372
  break;
2373
+ case "object":
2374
+ case "dict":
2375
+ /* jsoneditor Content shape */
2376
+ value = {json: is_object(value)? value : {}};
2377
+ break;
2120
2378
  case "blob":
2121
- // Return as it is, manage by jsoneditor
2122
- // TODO si es para editar, tiene que ser
2123
- // value = {json: value};
2379
+ /* jsoneditor Content shape (a blob may hold any json) */
2380
+ value = {json: (is_object(value) || is_array(value))? value : {}};
2381
+ break;
2382
+ case "array":
2383
+ case "list":
2384
+ /* jsoneditor Content shape */
2385
+ value = {json: is_array(value)? value : []};
2124
2386
  break;
2125
2387
  case "number":
2126
2388
  break;
@@ -2173,6 +2435,7 @@ function form_value_2_treedb_value(gobj, field_desc, value)
2173
2435
 
2174
2436
  switch(type) {
2175
2437
  case "fkey":
2438
+ value = build_fkey_ref(gobj, field_desc, value);
2176
2439
  break;
2177
2440
  case "hook":
2178
2441
  value = null;
@@ -2334,6 +2597,7 @@ function ac_load_record(gobj, event, kw, src)
2334
2597
 
2335
2598
  gobj_write_bool_attr(gobj, "initialized", false);
2336
2599
  let modified = set_form_values(gobj, gobj_read_attr(gobj, "template"), $form, kw);
2600
+ apply_form_mode(gobj, $form);
2337
2601
  set_changed_stated(gobj, modified);
2338
2602
  gobj_write_bool_attr(gobj, "initialized", true);
2339
2603
 
@@ -2619,6 +2883,13 @@ function create_gclass(gclass_name)
2619
2883
  ***************************************************************/
2620
2884
  function register_c_yui_form()
2621
2885
  {
2886
+ /* Idempotent: C_YUI_TREEDB_TOPIC_WITH_FORM auto-registers this
2887
+ * gclass for its hosted edit dialog, so an app that also registers
2888
+ * it explicitly (order-independent) must not trip
2889
+ * "GClass ALREADY created". */
2890
+ if(gclass_find_by_name(GCLASS_NAME, false)) {
2891
+ return 0;
2892
+ }
2622
2893
  return create_gclass(GCLASS_NAME);
2623
2894
  }
2624
2895