@liedekef/ftable 1.1.24 → 1.1.25

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(() => {
@@ -1270,7 +1311,6 @@ class FTableFormBuilder {
1270
1311
  const select = FTableDOMHelper.create('select', { attributes });
1271
1312
 
1272
1313
  if (field.options) {
1273
- // the field options are already the resolved ones
1274
1314
  this.populateSelectOptions(select, field.options, value);
1275
1315
  }
1276
1316
 
@@ -1588,6 +1628,11 @@ class FTable extends FTableEventEmitter {
1588
1628
  // Start resolving in background
1589
1629
  this.resolveAsyncFieldOptions().then(() => {
1590
1630
  // re-render dynamic options rows — no server call
1631
+ // this is needed so that once options are resolved, the table shows correct display values
1632
+ // why: load() can actually finish faster than option resolving (and calling refreshDisplayValues
1633
+ // there is then pointless, since the resolving hasn't finished yet),
1634
+ // so we need to do it when the options are actually resolved (here)
1635
+ // We could call await this.resolveAsyncFieldOptions() during load, but that would slow down the loading ...
1591
1636
  setTimeout(() => {
1592
1637
  this.refreshDisplayValues();
1593
1638
  }, 0);
@@ -1848,7 +1893,6 @@ class FTable extends FTableEventEmitter {
1848
1893
  });
1849
1894
 
1850
1895
  await Promise.all(promises);
1851
- // DON'T call refreshDisplayValues() here - let renderTableData do it
1852
1896
  }
1853
1897
 
1854
1898
  async refreshDisplayValues() {
@@ -1864,8 +1908,9 @@ class FTable extends FTableEventEmitter {
1864
1908
  if (!cell) continue;
1865
1909
 
1866
1910
  // Get table-specific options
1867
- const options = await this.formBuilder.getFieldOptions(fieldName, 'table');
1868
- const value = this.getDisplayText(row.recordData, fieldName, options);
1911
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
1912
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
1913
+ const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
1869
1914
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
1870
1915
  }
1871
1916
  }
@@ -2224,7 +2269,7 @@ class FTable extends FTableEventEmitter {
2224
2269
  DisplayText: displayText
2225
2270
  }));
2226
2271
  } else if (field.options) {
2227
- optionsSource = await this.formBuilder.resolveOptions(field, {}, 'search');
2272
+ optionsSource = await this.formBuilder.getFieldOptions(fieldName);
2228
2273
  }
2229
2274
 
2230
2275
  // Add empty option only if first option is not already empty
@@ -3127,7 +3172,9 @@ class FTable extends FTableEventEmitter {
3127
3172
  });
3128
3173
 
3129
3174
  this.refreshRowStyles();
3130
- this.refreshDisplayValues(); // for options that uses functions/url's
3175
+ // the next call might not do anything if option resolving hasn't finished yet
3176
+ // in fact, it might even not be needed, since we call it when option resolving finishes (see init)
3177
+ //this.refreshDisplayValues(); // for options that uses functions/url's
3131
3178
  }
3132
3179
 
3133
3180
  createTableRow(record) {
@@ -3189,7 +3236,9 @@ class FTable extends FTableEventEmitter {
3189
3236
 
3190
3237
  addDataCell(row, record, fieldName) {
3191
3238
  const field = this.options.fields[fieldName];
3192
- const value = this.getDisplayText(record, fieldName);
3239
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
3240
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
3241
+ const value = this.getDisplayText(record, fieldName, resolvedOptions);
3193
3242
 
3194
3243
  const cell = FTableDOMHelper.create('td', {
3195
3244
  className: `${field.listClass || ''} ${field.listClassEntry || ''}`,
@@ -3668,7 +3717,9 @@ class FTable extends FTableEventEmitter {
3668
3717
  if (!cell) return;
3669
3718
 
3670
3719
  // Get display text
3671
- const value = this.getDisplayText(row.recordData, fieldName);
3720
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
3721
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
3722
+ const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
3672
3723
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
3673
3724
  cell.className = `${field.listClass || ''} ${field.listClassEntry || ''}`.trim();
3674
3725
  });