@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 +131 -80
- package/ftable.js +131 -80
- package/ftable.min.js +2 -2
- package/ftable.umd.js +131 -80
- package/package.json +1 -1
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.
|
|
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
|
-
//
|
|
586
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
605
|
-
|
|
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 === '
|
|
609
|
-
|
|
610
|
-
|
|
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;
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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 (
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
|
1868
|
-
const
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
});
|