@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.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(() => {
|
|
@@ -1266,16 +1307,26 @@ class FTableFormBuilder {
|
|
|
1266
1307
|
class: field.inputClass || ''
|
|
1267
1308
|
};
|
|
1268
1309
|
|
|
1269
|
-
//
|
|
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(
|
|
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
|
|
1873
|
-
const
|
|
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
|
-
|
|
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 (
|
|
2070
|
-
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
});
|