@schukai/monster 4.71.0 → 4.72.0
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/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/source/components/datatable/datasource/rest.mjs +363 -1
- package/test/util/jsdom.mjs +20 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
## [4.72.0] - 2026-01-03
|
|
6
|
+
|
|
7
|
+
### Add Features
|
|
8
|
+
|
|
9
|
+
- Implement new fetch columns feature for issue [#360](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/360)
|
|
10
|
+
### Changes
|
|
11
|
+
|
|
12
|
+
- update tests
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
5
16
|
## [4.71.0] - 2026-01-03
|
|
6
17
|
|
|
7
18
|
### Add Features
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.
|
|
1
|
+
{"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.72.0"}
|
|
@@ -30,7 +30,7 @@ import { findElementWithIdUpwards } from "../../../dom/util.mjs";
|
|
|
30
30
|
import { Observer } from "../../../types/observer.mjs";
|
|
31
31
|
import { Pathfinder } from "../../../data/pathfinder.mjs";
|
|
32
32
|
import { fireCustomEvent } from "../../../dom/events.mjs";
|
|
33
|
-
import { isObject } from "../../../types/is.mjs";
|
|
33
|
+
import { isArray, isFunction, isObject, isString } from "../../../types/is.mjs";
|
|
34
34
|
|
|
35
35
|
export { Rest };
|
|
36
36
|
|
|
@@ -64,6 +64,8 @@ const intersectionObserverObserverSymbol = Symbol(
|
|
|
64
64
|
const filterObserverSymbol = Symbol("filterObserver");
|
|
65
65
|
const readRequestIdSymbol = Symbol("readRequestId");
|
|
66
66
|
const writeRequestIdSymbol = Symbol("writeRequestId");
|
|
67
|
+
const lookupCacheSymbol = Symbol("lookupCache");
|
|
68
|
+
const lookupPendingSymbol = Symbol("lookupPending");
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* A rest api datasource
|
|
@@ -174,6 +176,27 @@ class Rest extends Datasource {
|
|
|
174
176
|
validation: {
|
|
175
177
|
map: {},
|
|
176
178
|
},
|
|
179
|
+
|
|
180
|
+
lookups: {
|
|
181
|
+
enabled: false,
|
|
182
|
+
sourcePath: "dataset",
|
|
183
|
+
request: {
|
|
184
|
+
idsParam: "ids",
|
|
185
|
+
idsSeparator: ",",
|
|
186
|
+
},
|
|
187
|
+
response: {
|
|
188
|
+
path: "dataset",
|
|
189
|
+
id: "id",
|
|
190
|
+
},
|
|
191
|
+
format: {
|
|
192
|
+
template: "${name}",
|
|
193
|
+
marker: {
|
|
194
|
+
open: ["${"],
|
|
195
|
+
close: ["}"],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
columns: {},
|
|
199
|
+
},
|
|
177
200
|
});
|
|
178
201
|
}
|
|
179
202
|
|
|
@@ -315,6 +338,7 @@ class Rest extends Datasource {
|
|
|
315
338
|
return;
|
|
316
339
|
}
|
|
317
340
|
this[dataSourceSymbol].set(transformedPayload);
|
|
341
|
+
applyLookups.call(this, requestId).catch(() => {});
|
|
318
342
|
};
|
|
319
343
|
this[dataSourceSymbol].setOption("read", opt);
|
|
320
344
|
|
|
@@ -708,6 +732,344 @@ function handleValidationError(error) {
|
|
|
708
732
|
.catch(() => {});
|
|
709
733
|
}
|
|
710
734
|
|
|
735
|
+
/**
|
|
736
|
+
* @private
|
|
737
|
+
* @param {number} requestId
|
|
738
|
+
* @return {Promise<void>}
|
|
739
|
+
*/
|
|
740
|
+
async function applyLookups(requestId) {
|
|
741
|
+
if (!this.getOption("lookups.enabled", false)) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const columns = this.getOption("lookups.columns", {});
|
|
746
|
+
if (!isObject(columns)) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const columnEntries = Object.entries(columns).filter(([, cfg]) =>
|
|
751
|
+
isObject(cfg),
|
|
752
|
+
);
|
|
753
|
+
if (columnEntries.length === 0) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const resolverColumns = [];
|
|
758
|
+
const remoteColumns = [];
|
|
759
|
+
for (const [name, cfg] of columnEntries) {
|
|
760
|
+
if (isFunction(cfg.resolve)) {
|
|
761
|
+
resolverColumns.push([name, cfg]);
|
|
762
|
+
} else if (isString(cfg.url) && cfg.url !== "") {
|
|
763
|
+
remoteColumns.push([name, cfg]);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (resolverColumns.length > 0) {
|
|
768
|
+
updateLookupRows.call(this, requestId, (rows) => {
|
|
769
|
+
for (const [, cfg] of resolverColumns) {
|
|
770
|
+
const key = cfg.key;
|
|
771
|
+
const target = cfg.target;
|
|
772
|
+
if (!isString(key) || !isString(target)) {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
for (const row of rows) {
|
|
777
|
+
const entry = cfg.resolve(row[key], row);
|
|
778
|
+
if (!entry) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
row[target] = formatLookupValue.call(this, cfg, entry, row);
|
|
782
|
+
if (isString(cfg.loadingKey)) {
|
|
783
|
+
row[cfg.loadingKey] = false;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (remoteColumns.length === 0) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
await Promise.all(
|
|
795
|
+
remoteColumns.map(([name, cfg]) =>
|
|
796
|
+
resolveRemoteLookup.call(this, requestId, name, cfg),
|
|
797
|
+
),
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* @private
|
|
803
|
+
* @param {number} requestId
|
|
804
|
+
* @param {string} name
|
|
805
|
+
* @param {object} cfg
|
|
806
|
+
* @return {Promise<void>}
|
|
807
|
+
*/
|
|
808
|
+
async function resolveRemoteLookup(requestId, name, cfg) {
|
|
809
|
+
const key = cfg.key;
|
|
810
|
+
const target = cfg.target;
|
|
811
|
+
if (!isString(key) || !isString(target)) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const cache = getLookupCache.call(this, name);
|
|
816
|
+
const pending = getLookupPending.call(this, name);
|
|
817
|
+
|
|
818
|
+
const ids = collectLookupIds.call(this, key);
|
|
819
|
+
const missingIds = ids.filter((id) => !cache.has(id) && !pending.has(id));
|
|
820
|
+
|
|
821
|
+
if (missingIds.length === 0) {
|
|
822
|
+
applyLookupCache.call(this, requestId, cfg, cache);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
updateLookupRows.call(this, requestId, (rows) => {
|
|
827
|
+
if (!isString(cfg.loadingKey)) return;
|
|
828
|
+
for (const row of rows) {
|
|
829
|
+
if (missingIds.includes(String(row[key]))) {
|
|
830
|
+
row[cfg.loadingKey] = true;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
missingIds.forEach((id) => pending.add(id));
|
|
836
|
+
|
|
837
|
+
const response = await fetchLookupEntries.call(this, cfg, missingIds);
|
|
838
|
+
response.forEach((entry, id) => cache.set(id, entry));
|
|
839
|
+
missingIds.forEach((id) => pending.delete(id));
|
|
840
|
+
|
|
841
|
+
applyLookupCache.call(this, requestId, cfg, cache);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* @private
|
|
846
|
+
* @param {number} requestId
|
|
847
|
+
* @param {object} cfg
|
|
848
|
+
* @param {Map} cache
|
|
849
|
+
*/
|
|
850
|
+
function applyLookupCache(requestId, cfg, cache) {
|
|
851
|
+
const key = cfg.key;
|
|
852
|
+
const target = cfg.target;
|
|
853
|
+
|
|
854
|
+
updateLookupRows.call(this, requestId, (rows) => {
|
|
855
|
+
for (const row of rows) {
|
|
856
|
+
const entry = cache.get(String(row[key]));
|
|
857
|
+
if (entry) {
|
|
858
|
+
row[target] = formatLookupValue.call(this, cfg, entry, row);
|
|
859
|
+
}
|
|
860
|
+
if (isString(cfg.loadingKey)) {
|
|
861
|
+
row[cfg.loadingKey] = false;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* @private
|
|
869
|
+
* @param {string} key
|
|
870
|
+
* @return {string[]}
|
|
871
|
+
*/
|
|
872
|
+
function collectLookupIds(key) {
|
|
873
|
+
const rows = resolveLookupRows.call(this);
|
|
874
|
+
if (!isArray(rows)) {
|
|
875
|
+
return [];
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const ids = new Set();
|
|
879
|
+
for (const row of rows) {
|
|
880
|
+
const value = row?.[key];
|
|
881
|
+
if (value === undefined || value === null || value === "") {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (isArray(value)) {
|
|
885
|
+
value.forEach((entry) => ids.add(String(entry)));
|
|
886
|
+
} else {
|
|
887
|
+
ids.add(String(value));
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return Array.from(ids);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* @private
|
|
896
|
+
* @param {object} cfg
|
|
897
|
+
* @param {string[]} ids
|
|
898
|
+
* @return {Promise<Map<string, object>>}
|
|
899
|
+
*/
|
|
900
|
+
async function fetchLookupEntries(cfg, ids) {
|
|
901
|
+
const requestDefaults = this.getOption("lookups.request", {});
|
|
902
|
+
const request = {
|
|
903
|
+
...requestDefaults,
|
|
904
|
+
...(cfg.request || {}),
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
const init = isObject(request.init) ? request.init : {};
|
|
908
|
+
const url = buildLookupUrl(cfg.url, ids, request);
|
|
909
|
+
|
|
910
|
+
const response = await fetch(url, init);
|
|
911
|
+
if (!response.ok) {
|
|
912
|
+
return new Map();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
let payload;
|
|
916
|
+
try {
|
|
917
|
+
payload = await response.json();
|
|
918
|
+
} catch (_error) {
|
|
919
|
+
return new Map();
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const responseDefaults = this.getOption("lookups.response", {});
|
|
923
|
+
const responseConfig = {
|
|
924
|
+
...responseDefaults,
|
|
925
|
+
...(cfg.response || {}),
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
let entries = payload;
|
|
929
|
+
if (isString(responseConfig.path) && responseConfig.path !== "") {
|
|
930
|
+
entries = new Pathfinder(payload).getVia(responseConfig.path);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (!isArray(entries)) {
|
|
934
|
+
return new Map();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const idKey = responseConfig.id || "id";
|
|
938
|
+
const result = new Map();
|
|
939
|
+
for (const entry of entries) {
|
|
940
|
+
if (!entry || entry[idKey] === undefined || entry[idKey] === null) {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
result.set(String(entry[idKey]), entry);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return result;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* @private
|
|
951
|
+
* @param {string} url
|
|
952
|
+
* @param {string[]} ids
|
|
953
|
+
* @param {object} request
|
|
954
|
+
* @return {string}
|
|
955
|
+
*/
|
|
956
|
+
function buildLookupUrl(url, ids, request) {
|
|
957
|
+
const idsParam = request.idsParam || "ids";
|
|
958
|
+
const idsSeparator = request.idsSeparator || ",";
|
|
959
|
+
const idsValue = ids.join(idsSeparator);
|
|
960
|
+
|
|
961
|
+
if (url.includes("${")) {
|
|
962
|
+
const formatter = new Formatter({ ids: idsValue });
|
|
963
|
+
return formatter.format(url);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
967
|
+
return `${url}${separator}${encodeURIComponent(idsParam)}=${encodeURIComponent(
|
|
968
|
+
idsValue,
|
|
969
|
+
)}`;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* @private
|
|
974
|
+
* @param {object} cfg
|
|
975
|
+
* @param {object} entry
|
|
976
|
+
* @param {object} row
|
|
977
|
+
* @return {string}
|
|
978
|
+
*/
|
|
979
|
+
function formatLookupValue(cfg, entry, row) {
|
|
980
|
+
if (isFunction(cfg.format)) {
|
|
981
|
+
return cfg.format(entry, row);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const formatDefaults = this.getOption("lookups.format", {});
|
|
985
|
+
const format = isObject(cfg.format) ? cfg.format : {};
|
|
986
|
+
const template = format.template || formatDefaults.template || "${name}";
|
|
987
|
+
if (!isString(template)) {
|
|
988
|
+
return "";
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const formatter = new Formatter({ ...entry, row });
|
|
992
|
+
const marker = format.marker || formatDefaults.marker;
|
|
993
|
+
if (marker?.open) {
|
|
994
|
+
formatter.setMarker(marker.open, marker.close);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return formatter.format(template);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* @private
|
|
1002
|
+
* @param {number} requestId
|
|
1003
|
+
* @param {Function} update
|
|
1004
|
+
*/
|
|
1005
|
+
function updateLookupRows(requestId, update) {
|
|
1006
|
+
if (this[readRequestIdSymbol] !== requestId) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const data = this[dataSourceSymbol].get();
|
|
1011
|
+
const sourcePath = this.getOption("lookups.sourcePath", "dataset");
|
|
1012
|
+
const next = clone(data);
|
|
1013
|
+
const rows = resolveLookupRows.call(this, next, sourcePath);
|
|
1014
|
+
|
|
1015
|
+
if (!isArray(rows)) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
update(rows);
|
|
1020
|
+
this[dataSourceSymbol].set(next);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* @private
|
|
1025
|
+
* @param {object} [data]
|
|
1026
|
+
* @param {string} [sourcePath]
|
|
1027
|
+
* @return {Array|undefined}
|
|
1028
|
+
*/
|
|
1029
|
+
function resolveLookupRows(data, sourcePath) {
|
|
1030
|
+
const source = data || this[dataSourceSymbol].get();
|
|
1031
|
+
if (isString(sourcePath) && sourcePath !== "") {
|
|
1032
|
+
return new Pathfinder(source).getVia(sourcePath);
|
|
1033
|
+
}
|
|
1034
|
+
if (isArray(source)) {
|
|
1035
|
+
return source;
|
|
1036
|
+
}
|
|
1037
|
+
if (isObject(source) && isArray(source.dataset)) {
|
|
1038
|
+
return source.dataset;
|
|
1039
|
+
}
|
|
1040
|
+
return undefined;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* @private
|
|
1045
|
+
* @param {string} name
|
|
1046
|
+
* @return {Map<string, object>}
|
|
1047
|
+
*/
|
|
1048
|
+
function getLookupCache(name) {
|
|
1049
|
+
if (!this[lookupCacheSymbol]) {
|
|
1050
|
+
this[lookupCacheSymbol] = new Map();
|
|
1051
|
+
}
|
|
1052
|
+
if (!this[lookupCacheSymbol].has(name)) {
|
|
1053
|
+
this[lookupCacheSymbol].set(name, new Map());
|
|
1054
|
+
}
|
|
1055
|
+
return this[lookupCacheSymbol].get(name);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* @private
|
|
1060
|
+
* @param {string} name
|
|
1061
|
+
* @return {Set<string>}
|
|
1062
|
+
*/
|
|
1063
|
+
function getLookupPending(name) {
|
|
1064
|
+
if (!this[lookupPendingSymbol]) {
|
|
1065
|
+
this[lookupPendingSymbol] = new Map();
|
|
1066
|
+
}
|
|
1067
|
+
if (!this[lookupPendingSymbol].has(name)) {
|
|
1068
|
+
this[lookupPendingSymbol].set(name, new Set());
|
|
1069
|
+
}
|
|
1070
|
+
return this[lookupPendingSymbol].get(name);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
711
1073
|
/**
|
|
712
1074
|
* @private
|
|
713
1075
|
* @return {string}
|
package/test/util/jsdom.mjs
CHANGED
|
@@ -34,9 +34,13 @@ function initJSDOM(options) {
|
|
|
34
34
|
const {window} = new JSDOM(`<!DOCTYPE html><html lang="en"><head><title>Test</title></head><body><div id="mocks"></div></body></html>`, options);
|
|
35
35
|
|
|
36
36
|
g['window'] = window;
|
|
37
|
-
|
|
38
|
-
return new Promise((resolve, reject) =>
|
|
39
|
-
|
|
37
|
+
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
let settled = false;
|
|
40
|
+
|
|
41
|
+
const finalize = () => {
|
|
42
|
+
if (settled) return;
|
|
43
|
+
settled = true;
|
|
40
44
|
|
|
41
45
|
[
|
|
42
46
|
'Blob',
|
|
@@ -78,8 +82,6 @@ function initJSDOM(options) {
|
|
|
78
82
|
'XMLSerializer',
|
|
79
83
|
].forEach(key => {
|
|
80
84
|
try {
|
|
81
|
-
console.log("setting key", key);
|
|
82
|
-
|
|
83
85
|
g[key] = window[key]
|
|
84
86
|
} catch(e) {
|
|
85
87
|
console.error("Error setting key", key, e);
|
|
@@ -128,16 +130,26 @@ function initJSDOM(options) {
|
|
|
128
130
|
ensureStorage("sessionStorage");
|
|
129
131
|
|
|
130
132
|
resolve(g);
|
|
131
|
-
|
|
133
|
+
|
|
132
134
|
}).catch(e => {
|
|
133
135
|
console.error("Error loading dom-storage", e);
|
|
134
136
|
reject(e);
|
|
135
137
|
});
|
|
138
|
+
};
|
|
136
139
|
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
const onLoad = () => {
|
|
141
|
+
window.removeEventListener("load", onLoad);
|
|
142
|
+
finalize();
|
|
143
|
+
};
|
|
139
144
|
|
|
145
|
+
window.addEventListener("load", onLoad);
|
|
140
146
|
|
|
147
|
+
if (window.document.readyState === "complete") {
|
|
148
|
+
queueMicrotask(finalize);
|
|
149
|
+
} else {
|
|
150
|
+
setTimeout(finalize, 50);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
141
153
|
});
|
|
142
154
|
}
|
|
143
155
|
|