@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 +153 -86
- package/ftable.js +153 -86
- package/ftable.min.js +2 -2
- package/ftable.umd.js +153 -86
- 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(() => {
|
|
@@ -1261,16 +1302,26 @@ class FTableFormBuilder {
|
|
|
1261
1302
|
class: field.inputClass || ''
|
|
1262
1303
|
};
|
|
1263
1304
|
|
|
1264
|
-
//
|
|
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(
|
|
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
|
|
1868
|
-
const
|
|
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
|
-
|
|
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 (
|
|
2065
|
-
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
});
|