@liedekef/ftable 1.1.24 → 1.1.26

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/ftable.esm.js CHANGED
@@ -37,6 +37,7 @@ const FTABLE_DEFAULT_MESSAGES = {
37
37
  class FTableOptionsCache {
38
38
  constructor() {
39
39
  this.cache = new Map();
40
+ this.pendingRequests = new Map(); // Track ongoing requests
40
41
  }
41
42
 
42
43
  generateKey(url, params) {
@@ -76,6 +77,37 @@ class FTableOptionsCache {
76
77
  }
77
78
  }
78
79
 
80
+ async getOrCreate(url, params, fetchFn) {
81
+ const key = this.generateKey(url, params);
82
+
83
+ // Return cached result if available
84
+ const cached = this.cache.get(key);
85
+ if (cached) return cached;
86
+
87
+ // Check if same request is already in progress
88
+ if (this.pendingRequests.has(key)) {
89
+ // Wait for the existing request to complete
90
+ return await this.pendingRequests.get(key);
91
+ }
92
+
93
+ // Create new request
94
+ const requestPromise = (async () => {
95
+ try {
96
+ const result = await fetchFn();
97
+ this.cache.set(key, result);
98
+ return result;
99
+ } finally {
100
+ // Clean up pending request tracking
101
+ this.pendingRequests.delete(key);
102
+ }
103
+ })();
104
+
105
+ // Track this request
106
+ this.pendingRequests.set(key, requestPromise);
107
+
108
+ return await requestPromise;
109
+ }
110
+
79
111
  size() {
80
112
  return this.cache.size;
81
113
  }
@@ -141,6 +173,7 @@ class FTableLogger {
141
173
 
142
174
  const levelName = Object.keys(FTableLogger.LOG_LEVELS)
143
175
  .find(key => FTableLogger.LOG_LEVELS[key] === level);
176
+ console.trace();
144
177
  console.log(`fTable ${levelName}: ${message}`);
145
178
  }
146
179
 
@@ -567,7 +600,7 @@ class FTableFormBuilder {
567
600
  }
568
601
 
569
602
  // Determine if we should skip caching for this specific context
570
- const shouldSkipCache = this.shouldSkipCachingForContext(field, context, params);
603
+ const shouldSkipCache = this.shouldForceRefreshForContext(field, context, params);
571
604
  const cacheKey = this.generateOptionsCacheKey(context, params);
572
605
  // Skip cache if configured or forceRefresh requested
573
606
  if (!shouldSkipCache && !params.forceRefresh) {
@@ -582,10 +615,8 @@ class FTableFormBuilder {
582
615
  ...params
583
616
  }, context, shouldSkipCache);
584
617
 
585
- // Only cache if noCache is not enabled
586
- if (!shouldSkipCache) {
587
- this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
588
- }
618
+ // we store the resolved option always
619
+ this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
589
620
  return resolved;
590
621
  } catch (err) {
591
622
  console.error(`Failed to resolve options for ${fieldName} (${context}):`, err);
@@ -593,24 +624,49 @@ class FTableFormBuilder {
593
624
  }
594
625
  }
595
626
 
596
- // Helper method to determine caching behavior
597
- shouldSkipCachingForContext(field, context, params) {
598
- if (!field.noCache) return false;
599
-
600
- if (typeof field.noCache === 'boolean') {
601
- return field.noCache; // true = skip all contexts
627
+ /**
628
+ * Clear resolved options for specific field or all fields
629
+ * @param {string|null} fieldName - Field name to clear, or null for all fields
630
+ * @param {string|null} context - Context to clear ('table', 'create', 'edit'), or null for all contexts
631
+ */
632
+ clearResolvedOptions(fieldName = null, context = null) {
633
+ if (fieldName) {
634
+ // Clear specific field
635
+ if (this.resolvedFieldOptions.has(fieldName)) {
636
+ if (context) {
637
+ // Clear specific context for specific field
638
+ this.resolvedFieldOptions.get(fieldName)[context] = null;
639
+ } else {
640
+ // Clear all contexts for specific field
641
+ this.resolvedFieldOptions.set(fieldName, { table: null, create: null, edit: null });
642
+ }
643
+ }
644
+ } else {
645
+ // Clear all fields
646
+ if (context) {
647
+ // Clear specific context for all fields
648
+ this.resolvedFieldOptions.forEach((value, key) => {
649
+ this.resolvedFieldOptions.get(key)[context] = null;
650
+ });
651
+ } else {
652
+ // Clear everything
653
+ this.resolvedFieldOptions.forEach((value, key) => {
654
+ this.resolvedFieldOptions.set(key, { table: null, create: null, edit: null });
655
+ });
656
+ }
602
657
  }
658
+ }
603
659
 
604
- if (typeof field.noCache === 'function') {
605
- return field.noCache({ context, ...params });
606
- }
660
+ // Helper method to determine caching behavior
661
+ shouldForceRefreshForContext(field, context, params) {
662
+ // Rename to reflect what it actually does now
663
+ if (!field.noCache) return false;
607
664
 
608
- if (typeof field.noCache === 'object') {
609
- // Check if this specific context should skip cache
610
- return field.noCache[context] === true;
611
- }
665
+ if (typeof field.noCache === 'boolean') return field.noCache;
666
+ if (typeof field.noCache === 'function') return field.noCache({ context, ...params });
667
+ if (typeof field.noCache === 'object') return field.noCache[context] === true;
612
668
 
613
- return false; // Default to caching
669
+ return false;
614
670
  }
615
671
 
616
672
  generateOptionsCacheKey(context, params) {
@@ -639,18 +695,20 @@ class FTableFormBuilder {
639
695
  createFieldContainer(fieldName, field, record, formType) {
640
696
  // in this function, field.options already contains the resolved values
641
697
  const container = FTableDOMHelper.create('div', {
642
- className: 'ftable-input-field-container',
698
+ className: (field.type != 'hidden' ? 'ftable-input-field-container' : ''),
643
699
  attributes: {
644
700
  id: `ftable-input-field-container-div-${fieldName}`,
645
701
  }
646
702
  });
647
703
 
648
- // Label
649
- const label = FTableDOMHelper.create('div', {
650
- className: 'ftable-input-label',
651
- text: field.inputTitle || field.title,
652
- parent: container
653
- });
704
+ if (field.type != 'hidden') {
705
+ // Label
706
+ const label = FTableDOMHelper.create('div', {
707
+ className: 'ftable-input-label',
708
+ text: field.inputTitle || field.title,
709
+ parent: container
710
+ });
711
+ }
654
712
 
655
713
  // Input
656
714
  const inputContainer = this.createInput(fieldName, field, record[fieldName], formType);
@@ -663,9 +721,6 @@ class FTableFormBuilder {
663
721
 
664
722
  this.currentFormRecord = record;
665
723
 
666
- // Pre-resolve all options for fields depending on nothing, the others are handled down the road when dependancies are calculated
667
- await this.resolveFormFieldOptions(record, formType);
668
-
669
724
  const form = FTableDOMHelper.create('form', {
670
725
  className: `ftable-dialog-form ftable-${formType}-form`
671
726
  });
@@ -689,7 +744,6 @@ class FTableFormBuilder {
689
744
  fieldWithOptions.options = field.options;
690
745
  }
691
746
 
692
-
693
747
  const fieldContainer = this.createFieldContainer(fieldName, fieldWithOptions, record, formType);
694
748
  form.appendChild(fieldContainer);
695
749
  }
@@ -701,28 +755,6 @@ class FTableFormBuilder {
701
755
  return form;
702
756
  }
703
757
 
704
- async resolveFormFieldOptions(record, formType) {
705
- const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
706
- if (field.dependsOn) {
707
- // Dependent fields will be resolved when dependencies change
708
- return;
709
- }
710
-
711
- if (this.shouldResolveOptions(field.options)) {
712
- try {
713
- await this.getFieldOptions(fieldName, formType, {
714
- record,
715
- source: formType
716
- });
717
- } catch (err) {
718
- console.error(`Failed to resolve form options for ${fieldName}:`, err);
719
- }
720
- }
721
- });
722
-
723
- await Promise.all(promises);
724
- }
725
-
726
758
  shouldResolveOptions(options) {
727
759
  return options &&
728
760
  (typeof options === 'function' || typeof options === 'string') &&
@@ -815,27 +847,31 @@ class FTableFormBuilder {
815
847
  if (typeof url !== 'string') return [];
816
848
 
817
849
  // Only use cache if noCache is NOT set
818
- if (!noCache) {
819
- const cached = this.optionsCache.get(url, {});
820
- if (cached) return cached;
821
- }
822
-
823
- try {
824
- const response = this.options.forcePost
825
- ? await FTableHttpClient.post(url)
826
- : await FTableHttpClient.get(url);
827
- const options = response.Options || response.options || response || [];
828
-
829
- // Only cache if noCache is false
830
- if (!noCache) {
831
- this.optionsCache.set(url, {}, options);
850
+ if (noCache) {
851
+ try {
852
+ const response = this.options.forcePost
853
+ ? await FTableHttpClient.post(url)
854
+ : await FTableHttpClient.get(url);
855
+ return response.Options || response.options || response || [];
856
+ } catch (error) {
857
+ console.error(`Failed to load options from ${url}:`, error);
858
+ return [];
832
859
  }
833
-
834
- return options;
835
- } catch (error) {
836
- console.error(`Failed to load options from ${url}:`, error);
837
- return [];
860
+ } else {
861
+ // Use getOrCreate to prevent duplicate requests
862
+ return await this.optionsCache.getOrCreate(url, {}, async () => {
863
+ try {
864
+ const response = this.options.forcePost
865
+ ? await FTableHttpClient.post(url)
866
+ : await FTableHttpClient.get(url);
867
+ return response.Options || response.options || response || [];
868
+ } catch (error) {
869
+ console.error(`Failed to load options from ${url}:`, error);
870
+ return [];
871
+ }
872
+ });
838
873
  }
874
+
839
875
  }
840
876
 
841
877
  updateFieldCacheSetting(field, context, skipCache) {
@@ -935,6 +971,9 @@ class FTableFormBuilder {
935
971
  if (datalist) datalist.innerHTML = '';
936
972
  }
937
973
 
974
+ // Get current field value BEFORE resolving new options
975
+ const currentValue = input.value || record[fieldName] || '';
976
+
938
977
  // Resolve options with current context
939
978
  const params = {
940
979
  ...baseParams,
@@ -946,9 +985,11 @@ class FTableFormBuilder {
946
985
 
947
986
  // Populate the input
948
987
  if (input.tagName === 'SELECT') {
949
- this.populateSelectOptions(input, newOptions, '');
988
+ this.populateSelectOptions(input, newOptions, currentValue);
950
989
  } else if (input.tagName === 'INPUT' && input.list) {
951
990
  this.populateDatalistOptions(input.list, newOptions);
991
+ // For datalist, set the value directly
992
+ if (currentValue) input.value = currentValue;
952
993
  }
953
994
 
954
995
  setTimeout(() => {
@@ -1261,16 +1302,26 @@ class FTableFormBuilder {
1261
1302
  class: field.inputClass || ''
1262
1303
  };
1263
1304
 
1264
- // Apply inputAttributes
1305
+ // extra check for name and multiple
1306
+ let name = fieldName;
1307
+ // Apply inputAttributes from field definition
1265
1308
  if (field.inputAttributes) {
1309
+ let hasMultiple = false;
1310
+
1266
1311
  const parsed = this.parseInputAttributes(field.inputAttributes);
1267
1312
  Object.assign(attributes, parsed);
1313
+
1314
+ hasMultiple = parsed.multiple !== undefined && parsed.multiple !== false;
1315
+
1316
+ if (hasMultiple) {
1317
+ name = `${fieldName}[]`;
1318
+ }
1268
1319
  }
1320
+ attributes.name = name;
1269
1321
 
1270
1322
  const select = FTableDOMHelper.create('select', { attributes });
1271
1323
 
1272
1324
  if (field.options) {
1273
- // the field options are already the resolved ones
1274
1325
  this.populateSelectOptions(select, field.options, value);
1275
1326
  }
1276
1327
 
@@ -1307,7 +1358,7 @@ class FTableFormBuilder {
1307
1358
  // Apply inputAttributes
1308
1359
  if (field.inputAttributes) {
1309
1360
  const parsed = this.parseInputAttributes(field.inputAttributes);
1310
- Object.assign(attributes, parsed);
1361
+ Object.assign(radioAttributes, parsed);
1311
1362
  }
1312
1363
 
1313
1364
  const radio = FTableDOMHelper.create('input', {
@@ -1588,6 +1639,11 @@ class FTable extends FTableEventEmitter {
1588
1639
  // Start resolving in background
1589
1640
  this.resolveAsyncFieldOptions().then(() => {
1590
1641
  // re-render dynamic options rows — no server call
1642
+ // this is needed so that once options are resolved, the table shows correct display values
1643
+ // why: load() can actually finish faster than option resolving (and calling refreshDisplayValues
1644
+ // there is then pointless, since the resolving hasn't finished yet),
1645
+ // so we need to do it when the options are actually resolved (here)
1646
+ // We could call await this.resolveAsyncFieldOptions() during load, but that would slow down the loading ...
1591
1647
  setTimeout(() => {
1592
1648
  this.refreshDisplayValues();
1593
1649
  }, 0);
@@ -1848,7 +1904,6 @@ class FTable extends FTableEventEmitter {
1848
1904
  });
1849
1905
 
1850
1906
  await Promise.all(promises);
1851
- // DON'T call refreshDisplayValues() here - let renderTableData do it
1852
1907
  }
1853
1908
 
1854
1909
  async refreshDisplayValues() {
@@ -1864,8 +1919,9 @@ class FTable extends FTableEventEmitter {
1864
1919
  if (!cell) continue;
1865
1920
 
1866
1921
  // Get table-specific options
1867
- const options = await this.formBuilder.getFieldOptions(fieldName, 'table');
1868
- const value = this.getDisplayText(row.recordData, fieldName, options);
1922
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
1923
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
1924
+ const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
1869
1925
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
1870
1926
  }
1871
1927
  }
@@ -1972,7 +2028,7 @@ class FTable extends FTableEventEmitter {
1972
2028
 
1973
2029
  const textHeader = FTableDOMHelper.create('span', {
1974
2030
  className: 'ftable-column-header-text',
1975
- text: field.title || fieldName,
2031
+ html: field.title || fieldName,
1976
2032
  parent: container
1977
2033
  });
1978
2034
 
@@ -2059,14 +2115,19 @@ class FTable extends FTableEventEmitter {
2059
2115
  });
2060
2116
 
2061
2117
  let input;
2118
+ let searchType = 'text';
2062
2119
 
2063
2120
  // Auto-detect select type if options are provided
2064
- if (!field.type && field.options) {
2065
- field.type = 'select';
2121
+ if (field.searchType) {
2122
+ searchType = field.searchType;
2123
+ } else if (!field.type && field.options) {
2124
+ searchType = 'select';
2125
+ } else if (field.type) {
2126
+ searchType = field.type;
2066
2127
  }
2067
2128
  const fieldSearchName = 'ftable-toolbarsearch-' + fieldName;
2068
2129
 
2069
- switch (field.type) {
2130
+ switch (searchType) {
2070
2131
  case 'date':
2071
2132
  case 'datetime-local':
2072
2133
  if (typeof FDatepicker !== 'undefined') {
@@ -2224,7 +2285,7 @@ class FTable extends FTableEventEmitter {
2224
2285
  DisplayText: displayText
2225
2286
  }));
2226
2287
  } else if (field.options) {
2227
- optionsSource = await this.formBuilder.resolveOptions(field, {}, 'search');
2288
+ optionsSource = await this.formBuilder.getFieldOptions(fieldName);
2228
2289
  }
2229
2290
 
2230
2291
  // Add empty option only if first option is not already empty
@@ -3127,7 +3188,9 @@ class FTable extends FTableEventEmitter {
3127
3188
  });
3128
3189
 
3129
3190
  this.refreshRowStyles();
3130
- this.refreshDisplayValues(); // for options that uses functions/url's
3191
+ // the next call might not do anything if option resolving hasn't finished yet
3192
+ // in fact, it might even not be needed, since we call it when option resolving finishes (see init)
3193
+ //this.refreshDisplayValues(); // for options that uses functions/url's
3131
3194
  }
3132
3195
 
3133
3196
  createTableRow(record) {
@@ -3189,7 +3252,9 @@ class FTable extends FTableEventEmitter {
3189
3252
 
3190
3253
  addDataCell(row, record, fieldName) {
3191
3254
  const field = this.options.fields[fieldName];
3192
- const value = this.getDisplayText(record, fieldName);
3255
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
3256
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
3257
+ const value = this.getDisplayText(record, fieldName, resolvedOptions);
3193
3258
 
3194
3259
  const cell = FTableDOMHelper.create('td', {
3195
3260
  className: `${field.listClass || ''} ${field.listClassEntry || ''}`,
@@ -3668,7 +3733,9 @@ class FTable extends FTableEventEmitter {
3668
3733
  if (!cell) return;
3669
3734
 
3670
3735
  // Get display text
3671
- const value = this.getDisplayText(row.recordData, fieldName);
3736
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
3737
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
3738
+ const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
3672
3739
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
3673
3740
  cell.className = `${field.listClass || ''} ${field.listClassEntry || ''}`.trim();
3674
3741
  });