@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.umd.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.
|
|
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
|
-
//
|
|
591
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
610
|
-
|
|
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 === '
|
|
614
|
-
|
|
615
|
-
|
|
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;
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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 (
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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(() => {
|
|
@@ -1275,7 +1316,6 @@ class FTableFormBuilder {
|
|
|
1275
1316
|
const select = FTableDOMHelper.create('select', { attributes });
|
|
1276
1317
|
|
|
1277
1318
|
if (field.options) {
|
|
1278
|
-
// the field options are already the resolved ones
|
|
1279
1319
|
this.populateSelectOptions(select, field.options, value);
|
|
1280
1320
|
}
|
|
1281
1321
|
|
|
@@ -1593,6 +1633,11 @@ class FTable extends FTableEventEmitter {
|
|
|
1593
1633
|
// Start resolving in background
|
|
1594
1634
|
this.resolveAsyncFieldOptions().then(() => {
|
|
1595
1635
|
// re-render dynamic options rows — no server call
|
|
1636
|
+
// this is needed so that once options are resolved, the table shows correct display values
|
|
1637
|
+
// why: load() can actually finish faster than option resolving (and calling refreshDisplayValues
|
|
1638
|
+
// there is then pointless, since the resolving hasn't finished yet),
|
|
1639
|
+
// so we need to do it when the options are actually resolved (here)
|
|
1640
|
+
// We could call await this.resolveAsyncFieldOptions() during load, but that would slow down the loading ...
|
|
1596
1641
|
setTimeout(() => {
|
|
1597
1642
|
this.refreshDisplayValues();
|
|
1598
1643
|
}, 0);
|
|
@@ -1853,7 +1898,6 @@ class FTable extends FTableEventEmitter {
|
|
|
1853
1898
|
});
|
|
1854
1899
|
|
|
1855
1900
|
await Promise.all(promises);
|
|
1856
|
-
// DON'T call refreshDisplayValues() here - let renderTableData do it
|
|
1857
1901
|
}
|
|
1858
1902
|
|
|
1859
1903
|
async refreshDisplayValues() {
|
|
@@ -1869,8 +1913,9 @@ class FTable extends FTableEventEmitter {
|
|
|
1869
1913
|
if (!cell) continue;
|
|
1870
1914
|
|
|
1871
1915
|
// Get table-specific options
|
|
1872
|
-
const
|
|
1873
|
-
const
|
|
1916
|
+
const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
|
|
1917
|
+
const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
|
|
1918
|
+
const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
|
|
1874
1919
|
cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
|
|
1875
1920
|
}
|
|
1876
1921
|
}
|
|
@@ -2229,7 +2274,7 @@ class FTable extends FTableEventEmitter {
|
|
|
2229
2274
|
DisplayText: displayText
|
|
2230
2275
|
}));
|
|
2231
2276
|
} else if (field.options) {
|
|
2232
|
-
optionsSource = await this.formBuilder.
|
|
2277
|
+
optionsSource = await this.formBuilder.getFieldOptions(fieldName);
|
|
2233
2278
|
}
|
|
2234
2279
|
|
|
2235
2280
|
// Add empty option only if first option is not already empty
|
|
@@ -3132,7 +3177,9 @@ class FTable extends FTableEventEmitter {
|
|
|
3132
3177
|
});
|
|
3133
3178
|
|
|
3134
3179
|
this.refreshRowStyles();
|
|
3135
|
-
|
|
3180
|
+
// the next call might not do anything if option resolving hasn't finished yet
|
|
3181
|
+
// in fact, it might even not be needed, since we call it when option resolving finishes (see init)
|
|
3182
|
+
//this.refreshDisplayValues(); // for options that uses functions/url's
|
|
3136
3183
|
}
|
|
3137
3184
|
|
|
3138
3185
|
createTableRow(record) {
|
|
@@ -3194,7 +3241,9 @@ class FTable extends FTableEventEmitter {
|
|
|
3194
3241
|
|
|
3195
3242
|
addDataCell(row, record, fieldName) {
|
|
3196
3243
|
const field = this.options.fields[fieldName];
|
|
3197
|
-
const
|
|
3244
|
+
const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
|
|
3245
|
+
const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
|
|
3246
|
+
const value = this.getDisplayText(record, fieldName, resolvedOptions);
|
|
3198
3247
|
|
|
3199
3248
|
const cell = FTableDOMHelper.create('td', {
|
|
3200
3249
|
className: `${field.listClass || ''} ${field.listClassEntry || ''}`,
|
|
@@ -3673,7 +3722,9 @@ class FTable extends FTableEventEmitter {
|
|
|
3673
3722
|
if (!cell) return;
|
|
3674
3723
|
|
|
3675
3724
|
// Get display text
|
|
3676
|
-
const
|
|
3725
|
+
const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
|
|
3726
|
+
const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
|
|
3727
|
+
const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
|
|
3677
3728
|
cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
|
|
3678
3729
|
cell.className = `${field.listClass || ''} ${field.listClassEntry || ''}`.trim();
|
|
3679
3730
|
});
|