@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.js CHANGED
@@ -42,6 +42,7 @@
42
42
  class FTableOptionsCache {
43
43
  constructor() {
44
44
  this.cache = new Map();
45
+ this.pendingRequests = new Map(); // Track ongoing requests
45
46
  }
46
47
 
47
48
  generateKey(url, params) {
@@ -81,6 +82,37 @@ class FTableOptionsCache {
81
82
  }
82
83
  }
83
84
 
85
+ async getOrCreate(url, params, fetchFn) {
86
+ const key = this.generateKey(url, params);
87
+
88
+ // Return cached result if available
89
+ const cached = this.cache.get(key);
90
+ if (cached) return cached;
91
+
92
+ // Check if same request is already in progress
93
+ if (this.pendingRequests.has(key)) {
94
+ // Wait for the existing request to complete
95
+ return await this.pendingRequests.get(key);
96
+ }
97
+
98
+ // Create new request
99
+ const requestPromise = (async () => {
100
+ try {
101
+ const result = await fetchFn();
102
+ this.cache.set(key, result);
103
+ return result;
104
+ } finally {
105
+ // Clean up pending request tracking
106
+ this.pendingRequests.delete(key);
107
+ }
108
+ })();
109
+
110
+ // Track this request
111
+ this.pendingRequests.set(key, requestPromise);
112
+
113
+ return await requestPromise;
114
+ }
115
+
84
116
  size() {
85
117
  return this.cache.size;
86
118
  }
@@ -146,6 +178,7 @@ class FTableLogger {
146
178
 
147
179
  const levelName = Object.keys(FTableLogger.LOG_LEVELS)
148
180
  .find(key => FTableLogger.LOG_LEVELS[key] === level);
181
+ console.trace();
149
182
  console.log(`fTable ${levelName}: ${message}`);
150
183
  }
151
184
 
@@ -572,7 +605,7 @@ class FTableFormBuilder {
572
605
  }
573
606
 
574
607
  // Determine if we should skip caching for this specific context
575
- const shouldSkipCache = this.shouldSkipCachingForContext(field, context, params);
608
+ const shouldSkipCache = this.shouldForceRefreshForContext(field, context, params);
576
609
  const cacheKey = this.generateOptionsCacheKey(context, params);
577
610
  // Skip cache if configured or forceRefresh requested
578
611
  if (!shouldSkipCache && !params.forceRefresh) {
@@ -587,10 +620,8 @@ class FTableFormBuilder {
587
620
  ...params
588
621
  }, context, shouldSkipCache);
589
622
 
590
- // Only cache if noCache is not enabled
591
- if (!shouldSkipCache) {
592
- this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
593
- }
623
+ // we store the resolved option always
624
+ this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
594
625
  return resolved;
595
626
  } catch (err) {
596
627
  console.error(`Failed to resolve options for ${fieldName} (${context}):`, err);
@@ -598,24 +629,49 @@ class FTableFormBuilder {
598
629
  }
599
630
  }
600
631
 
601
- // Helper method to determine caching behavior
602
- shouldSkipCachingForContext(field, context, params) {
603
- if (!field.noCache) return false;
604
-
605
- if (typeof field.noCache === 'boolean') {
606
- return field.noCache; // true = skip all contexts
632
+ /**
633
+ * Clear resolved options for specific field or all fields
634
+ * @param {string|null} fieldName - Field name to clear, or null for all fields
635
+ * @param {string|null} context - Context to clear ('table', 'create', 'edit'), or null for all contexts
636
+ */
637
+ clearResolvedOptions(fieldName = null, context = null) {
638
+ if (fieldName) {
639
+ // Clear specific field
640
+ if (this.resolvedFieldOptions.has(fieldName)) {
641
+ if (context) {
642
+ // Clear specific context for specific field
643
+ this.resolvedFieldOptions.get(fieldName)[context] = null;
644
+ } else {
645
+ // Clear all contexts for specific field
646
+ this.resolvedFieldOptions.set(fieldName, { table: null, create: null, edit: null });
647
+ }
648
+ }
649
+ } else {
650
+ // Clear all fields
651
+ if (context) {
652
+ // Clear specific context for all fields
653
+ this.resolvedFieldOptions.forEach((value, key) => {
654
+ this.resolvedFieldOptions.get(key)[context] = null;
655
+ });
656
+ } else {
657
+ // Clear everything
658
+ this.resolvedFieldOptions.forEach((value, key) => {
659
+ this.resolvedFieldOptions.set(key, { table: null, create: null, edit: null });
660
+ });
661
+ }
607
662
  }
663
+ }
608
664
 
609
- if (typeof field.noCache === 'function') {
610
- return field.noCache({ context, ...params });
611
- }
665
+ // Helper method to determine caching behavior
666
+ shouldForceRefreshForContext(field, context, params) {
667
+ // Rename to reflect what it actually does now
668
+ if (!field.noCache) return false;
612
669
 
613
- if (typeof field.noCache === 'object') {
614
- // Check if this specific context should skip cache
615
- return field.noCache[context] === true;
616
- }
670
+ if (typeof field.noCache === 'boolean') return field.noCache;
671
+ if (typeof field.noCache === 'function') return field.noCache({ context, ...params });
672
+ if (typeof field.noCache === 'object') return field.noCache[context] === true;
617
673
 
618
- return false; // Default to caching
674
+ return false;
619
675
  }
620
676
 
621
677
  generateOptionsCacheKey(context, params) {
@@ -644,18 +700,20 @@ class FTableFormBuilder {
644
700
  createFieldContainer(fieldName, field, record, formType) {
645
701
  // in this function, field.options already contains the resolved values
646
702
  const container = FTableDOMHelper.create('div', {
647
- className: 'ftable-input-field-container',
703
+ className: (field.type != 'hidden' ? 'ftable-input-field-container' : ''),
648
704
  attributes: {
649
705
  id: `ftable-input-field-container-div-${fieldName}`,
650
706
  }
651
707
  });
652
708
 
653
- // Label
654
- const label = FTableDOMHelper.create('div', {
655
- className: 'ftable-input-label',
656
- text: field.inputTitle || field.title,
657
- parent: container
658
- });
709
+ if (field.type != 'hidden') {
710
+ // Label
711
+ const label = FTableDOMHelper.create('div', {
712
+ className: 'ftable-input-label',
713
+ text: field.inputTitle || field.title,
714
+ parent: container
715
+ });
716
+ }
659
717
 
660
718
  // Input
661
719
  const inputContainer = this.createInput(fieldName, field, record[fieldName], formType);
@@ -668,9 +726,6 @@ class FTableFormBuilder {
668
726
 
669
727
  this.currentFormRecord = record;
670
728
 
671
- // Pre-resolve all options for fields depending on nothing, the others are handled down the road when dependancies are calculated
672
- await this.resolveFormFieldOptions(record, formType);
673
-
674
729
  const form = FTableDOMHelper.create('form', {
675
730
  className: `ftable-dialog-form ftable-${formType}-form`
676
731
  });
@@ -694,7 +749,6 @@ class FTableFormBuilder {
694
749
  fieldWithOptions.options = field.options;
695
750
  }
696
751
 
697
-
698
752
  const fieldContainer = this.createFieldContainer(fieldName, fieldWithOptions, record, formType);
699
753
  form.appendChild(fieldContainer);
700
754
  }
@@ -706,28 +760,6 @@ class FTableFormBuilder {
706
760
  return form;
707
761
  }
708
762
 
709
- async resolveFormFieldOptions(record, formType) {
710
- const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
711
- if (field.dependsOn) {
712
- // Dependent fields will be resolved when dependencies change
713
- return;
714
- }
715
-
716
- if (this.shouldResolveOptions(field.options)) {
717
- try {
718
- await this.getFieldOptions(fieldName, formType, {
719
- record,
720
- source: formType
721
- });
722
- } catch (err) {
723
- console.error(`Failed to resolve form options for ${fieldName}:`, err);
724
- }
725
- }
726
- });
727
-
728
- await Promise.all(promises);
729
- }
730
-
731
763
  shouldResolveOptions(options) {
732
764
  return options &&
733
765
  (typeof options === 'function' || typeof options === 'string') &&
@@ -820,27 +852,31 @@ class FTableFormBuilder {
820
852
  if (typeof url !== 'string') return [];
821
853
 
822
854
  // Only use cache if noCache is NOT set
823
- if (!noCache) {
824
- const cached = this.optionsCache.get(url, {});
825
- if (cached) return cached;
826
- }
827
-
828
- try {
829
- const response = this.options.forcePost
830
- ? await FTableHttpClient.post(url)
831
- : await FTableHttpClient.get(url);
832
- const options = response.Options || response.options || response || [];
833
-
834
- // Only cache if noCache is false
835
- if (!noCache) {
836
- this.optionsCache.set(url, {}, options);
855
+ if (noCache) {
856
+ try {
857
+ const response = this.options.forcePost
858
+ ? await FTableHttpClient.post(url)
859
+ : await FTableHttpClient.get(url);
860
+ return response.Options || response.options || response || [];
861
+ } catch (error) {
862
+ console.error(`Failed to load options from ${url}:`, error);
863
+ return [];
837
864
  }
838
-
839
- return options;
840
- } catch (error) {
841
- console.error(`Failed to load options from ${url}:`, error);
842
- return [];
865
+ } else {
866
+ // Use getOrCreate to prevent duplicate requests
867
+ return await this.optionsCache.getOrCreate(url, {}, async () => {
868
+ try {
869
+ const response = this.options.forcePost
870
+ ? await FTableHttpClient.post(url)
871
+ : await FTableHttpClient.get(url);
872
+ return response.Options || response.options || response || [];
873
+ } catch (error) {
874
+ console.error(`Failed to load options from ${url}:`, error);
875
+ return [];
876
+ }
877
+ });
843
878
  }
879
+
844
880
  }
845
881
 
846
882
  updateFieldCacheSetting(field, context, skipCache) {
@@ -940,6 +976,9 @@ class FTableFormBuilder {
940
976
  if (datalist) datalist.innerHTML = '';
941
977
  }
942
978
 
979
+ // Get current field value BEFORE resolving new options
980
+ const currentValue = input.value || record[fieldName] || '';
981
+
943
982
  // Resolve options with current context
944
983
  const params = {
945
984
  ...baseParams,
@@ -951,9 +990,11 @@ class FTableFormBuilder {
951
990
 
952
991
  // Populate the input
953
992
  if (input.tagName === 'SELECT') {
954
- this.populateSelectOptions(input, newOptions, '');
993
+ this.populateSelectOptions(input, newOptions, currentValue);
955
994
  } else if (input.tagName === 'INPUT' && input.list) {
956
995
  this.populateDatalistOptions(input.list, newOptions);
996
+ // For datalist, set the value directly
997
+ if (currentValue) input.value = currentValue;
957
998
  }
958
999
 
959
1000
  setTimeout(() => {
@@ -1266,16 +1307,26 @@ class FTableFormBuilder {
1266
1307
  class: field.inputClass || ''
1267
1308
  };
1268
1309
 
1269
- // Apply inputAttributes
1310
+ // extra check for name and multiple
1311
+ let name = fieldName;
1312
+ // Apply inputAttributes from field definition
1270
1313
  if (field.inputAttributes) {
1314
+ let hasMultiple = false;
1315
+
1271
1316
  const parsed = this.parseInputAttributes(field.inputAttributes);
1272
1317
  Object.assign(attributes, parsed);
1318
+
1319
+ hasMultiple = parsed.multiple !== undefined && parsed.multiple !== false;
1320
+
1321
+ if (hasMultiple) {
1322
+ name = `${fieldName}[]`;
1323
+ }
1273
1324
  }
1325
+ attributes.name = name;
1274
1326
 
1275
1327
  const select = FTableDOMHelper.create('select', { attributes });
1276
1328
 
1277
1329
  if (field.options) {
1278
- // the field options are already the resolved ones
1279
1330
  this.populateSelectOptions(select, field.options, value);
1280
1331
  }
1281
1332
 
@@ -1312,7 +1363,7 @@ class FTableFormBuilder {
1312
1363
  // Apply inputAttributes
1313
1364
  if (field.inputAttributes) {
1314
1365
  const parsed = this.parseInputAttributes(field.inputAttributes);
1315
- Object.assign(attributes, parsed);
1366
+ Object.assign(radioAttributes, parsed);
1316
1367
  }
1317
1368
 
1318
1369
  const radio = FTableDOMHelper.create('input', {
@@ -1593,6 +1644,11 @@ class FTable extends FTableEventEmitter {
1593
1644
  // Start resolving in background
1594
1645
  this.resolveAsyncFieldOptions().then(() => {
1595
1646
  // re-render dynamic options rows — no server call
1647
+ // this is needed so that once options are resolved, the table shows correct display values
1648
+ // why: load() can actually finish faster than option resolving (and calling refreshDisplayValues
1649
+ // there is then pointless, since the resolving hasn't finished yet),
1650
+ // so we need to do it when the options are actually resolved (here)
1651
+ // We could call await this.resolveAsyncFieldOptions() during load, but that would slow down the loading ...
1596
1652
  setTimeout(() => {
1597
1653
  this.refreshDisplayValues();
1598
1654
  }, 0);
@@ -1853,7 +1909,6 @@ class FTable extends FTableEventEmitter {
1853
1909
  });
1854
1910
 
1855
1911
  await Promise.all(promises);
1856
- // DON'T call refreshDisplayValues() here - let renderTableData do it
1857
1912
  }
1858
1913
 
1859
1914
  async refreshDisplayValues() {
@@ -1869,8 +1924,9 @@ class FTable extends FTableEventEmitter {
1869
1924
  if (!cell) continue;
1870
1925
 
1871
1926
  // Get table-specific options
1872
- const options = await this.formBuilder.getFieldOptions(fieldName, 'table');
1873
- const value = this.getDisplayText(row.recordData, fieldName, options);
1927
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
1928
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
1929
+ const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
1874
1930
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
1875
1931
  }
1876
1932
  }
@@ -1977,7 +2033,7 @@ class FTable extends FTableEventEmitter {
1977
2033
 
1978
2034
  const textHeader = FTableDOMHelper.create('span', {
1979
2035
  className: 'ftable-column-header-text',
1980
- text: field.title || fieldName,
2036
+ html: field.title || fieldName,
1981
2037
  parent: container
1982
2038
  });
1983
2039
 
@@ -2064,14 +2120,19 @@ class FTable extends FTableEventEmitter {
2064
2120
  });
2065
2121
 
2066
2122
  let input;
2123
+ let searchType = 'text';
2067
2124
 
2068
2125
  // Auto-detect select type if options are provided
2069
- if (!field.type && field.options) {
2070
- field.type = 'select';
2126
+ if (field.searchType) {
2127
+ searchType = field.searchType;
2128
+ } else if (!field.type && field.options) {
2129
+ searchType = 'select';
2130
+ } else if (field.type) {
2131
+ searchType = field.type;
2071
2132
  }
2072
2133
  const fieldSearchName = 'ftable-toolbarsearch-' + fieldName;
2073
2134
 
2074
- switch (field.type) {
2135
+ switch (searchType) {
2075
2136
  case 'date':
2076
2137
  case 'datetime-local':
2077
2138
  if (typeof FDatepicker !== 'undefined') {
@@ -2229,7 +2290,7 @@ class FTable extends FTableEventEmitter {
2229
2290
  DisplayText: displayText
2230
2291
  }));
2231
2292
  } else if (field.options) {
2232
- optionsSource = await this.formBuilder.resolveOptions(field, {}, 'search');
2293
+ optionsSource = await this.formBuilder.getFieldOptions(fieldName);
2233
2294
  }
2234
2295
 
2235
2296
  // Add empty option only if first option is not already empty
@@ -3132,7 +3193,9 @@ class FTable extends FTableEventEmitter {
3132
3193
  });
3133
3194
 
3134
3195
  this.refreshRowStyles();
3135
- this.refreshDisplayValues(); // for options that uses functions/url's
3196
+ // the next call might not do anything if option resolving hasn't finished yet
3197
+ // in fact, it might even not be needed, since we call it when option resolving finishes (see init)
3198
+ //this.refreshDisplayValues(); // for options that uses functions/url's
3136
3199
  }
3137
3200
 
3138
3201
  createTableRow(record) {
@@ -3194,7 +3257,9 @@ class FTable extends FTableEventEmitter {
3194
3257
 
3195
3258
  addDataCell(row, record, fieldName) {
3196
3259
  const field = this.options.fields[fieldName];
3197
- const value = this.getDisplayText(record, fieldName);
3260
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
3261
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
3262
+ const value = this.getDisplayText(record, fieldName, resolvedOptions);
3198
3263
 
3199
3264
  const cell = FTableDOMHelper.create('td', {
3200
3265
  className: `${field.listClass || ''} ${field.listClassEntry || ''}`,
@@ -3673,7 +3738,9 @@ class FTable extends FTableEventEmitter {
3673
3738
  if (!cell) return;
3674
3739
 
3675
3740
  // Get display text
3676
- const value = this.getDisplayText(row.recordData, fieldName);
3741
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
3742
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
3743
+ const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
3677
3744
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
3678
3745
  cell.className = `${field.listClass || ''} ${field.listClassEntry || ''}`.trim();
3679
3746
  });