@schukai/monster 4.142.4 → 4.143.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/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"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.142.4"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"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.143.0"}
@@ -76,7 +76,7 @@ import {
76
76
  import { getDocumentTranslations } from "../../i18n/translations.mjs";
77
77
  import "../state/state.mjs";
78
78
  import "../host/collapse.mjs";
79
- import { generateUniqueConfigKey } from "../host/util.mjs";
79
+ import { generateComponentConfigKey } from "../host/util.mjs";
80
80
 
81
81
  import "./datasource/dom.mjs";
82
82
  import "./datasource/rest.mjs";
@@ -279,6 +279,10 @@ class DataTable extends CustomElement {
279
279
  "row-key": null,
280
280
  "filter-id": null,
281
281
  },
282
+
283
+ config: {
284
+ instanceKey: null,
285
+ },
282
286
  },
283
287
  initOptionsFromArguments.call(this),
284
288
  );
@@ -659,15 +663,21 @@ class DataTable extends CustomElement {
659
663
  * @return {string}
660
664
  */
661
665
  function getColumnVisibilityConfigKey() {
662
- return generateUniqueConfigKey("datatable", this?.id, "columns-visibility");
666
+ return generateComponentConfigKey("datatable", this?.id, "columns-visibility", {
667
+ instanceKey: this.getOption("config.instanceKey"),
668
+ });
663
669
  }
664
670
 
665
671
  /**
666
672
  * @private
667
673
  * @return {string}
668
674
  */
669
- function getFilterConfigKey() {
670
- return generateUniqueConfigKey("datatable", this?.id, "filter");
675
+ function hasConfigIdentity() {
676
+ return (
677
+ (isString(this.id) && this.id !== "") ||
678
+ (isString(this.getOption("config.instanceKey")) &&
679
+ this.getOption("config.instanceKey").trim() !== "")
680
+ );
671
681
  }
672
682
 
673
683
  /**
@@ -682,8 +692,11 @@ function getHostConfig(callback) {
682
692
  return Promise.resolve({});
683
693
  }
684
694
 
685
- if (!this.id) {
686
- addErrorAttribute(this, "no id found; id is required for config");
695
+ if (!hasConfigIdentity.call(this)) {
696
+ addErrorAttribute(
697
+ this,
698
+ "no id or config.instanceKey found; one is required for config",
699
+ );
687
700
  return Promise.resolve({});
688
701
  }
689
702
 
@@ -782,7 +795,7 @@ function updateConfigColumnBar() {
782
795
  }
783
796
 
784
797
  const host = findElementWithSelectorUpwards(this, "monster-host");
785
- if (!(host && this.id)) {
798
+ if (!(host && hasConfigIdentity.call(this))) {
786
799
  return;
787
800
  }
788
801
  const configKey = getColumnVisibilityConfigKey.call(this);
@@ -1616,7 +1629,9 @@ function getTranslations() {
1616
1629
  * @return {string}
1617
1630
  */
1618
1631
  export function getStoredOrderConfigKey() {
1619
- return generateUniqueConfigKey("datatable", this?.id, "stored-order");
1632
+ return generateComponentConfigKey("datatable", this?.id, "stored-order", {
1633
+ instanceKey: this.getOption("config.instanceKey"),
1634
+ });
1620
1635
  }
1621
1636
 
1622
1637
  /**
@@ -1629,7 +1644,7 @@ function storeOrderStatement(doFetch) {
1629
1644
  setDataSource.call(this, { order: statement }, doFetch);
1630
1645
 
1631
1646
  const host = findElementWithSelectorUpwards(this, "monster-host");
1632
- if (!(host && this.id)) {
1647
+ if (!(host && hasConfigIdentity.call(this))) {
1633
1648
  return;
1634
1649
  }
1635
1650
 
@@ -12,14 +12,16 @@
12
12
  * SPDX-License-Identifier: AGPL-3.0
13
13
  */
14
14
 
15
- import { generateUniqueConfigKey } from "../../host/util.mjs";
15
+ import { generateComponentConfigKey } from "../../host/util.mjs";
16
16
 
17
17
  /**
18
18
  * @private
19
19
  * @return {string}
20
20
  */
21
21
  export function getFilterConfigKey() {
22
- return generateUniqueConfigKey("datatable", this?.id, "filter");
22
+ return generateComponentConfigKey("datatable", this?.id, "filter", {
23
+ instanceKey: this.getOption("config.instanceKey"),
24
+ });
23
25
  }
24
26
 
25
27
  /**
@@ -27,7 +29,9 @@ export function getFilterConfigKey() {
27
29
  * @return {string}
28
30
  */
29
31
  export function getStoredFilterConfigKey() {
30
- return generateUniqueConfigKey("datatable", this?.id, "stored-filter");
32
+ return generateComponentConfigKey("datatable", this?.id, "stored-filter", {
33
+ instanceKey: this.getOption("config.instanceKey"),
34
+ });
31
35
  }
32
36
 
33
37
  /**
@@ -293,6 +293,10 @@ class Filter extends CustomElement {
293
293
  selector: "",
294
294
  },
295
295
 
296
+ config: {
297
+ instanceKey: null,
298
+ },
299
+
296
300
  timeouts: {
297
301
  message: 4000,
298
302
  },
@@ -915,7 +919,7 @@ function initEventHandler() {
915
919
  .then(() => {
916
920
  const configKey = getStoredFilterConfigKey.call(self);
917
921
  const host = getDocument().querySelector("monster-host");
918
- if (!host) {
922
+ if (!(host && hasConfigIdentity.call(self))) {
919
923
  return;
920
924
  }
921
925
 
@@ -1070,7 +1074,7 @@ function initTabEvents() {
1070
1074
  }
1071
1075
 
1072
1076
  const host = findElementWithSelectorUpwards(this, "monster-host");
1073
- if (!(host && this.id)) {
1077
+ if (!(host && hasConfigIdentity.call(this))) {
1074
1078
  return;
1075
1079
  }
1076
1080
 
@@ -1111,7 +1115,7 @@ function updateFilterTabs() {
1111
1115
  }
1112
1116
 
1113
1117
  const host = findElementWithSelectorUpwards(this, "monster-host");
1114
- if (!(host && this.id)) {
1118
+ if (!(host && hasConfigIdentity.call(this))) {
1115
1119
  return;
1116
1120
  }
1117
1121
 
@@ -1580,7 +1584,7 @@ function getControlValuesFromLabel(label) {
1580
1584
  function initFromConfig() {
1581
1585
  const host = findElementWithSelectorUpwards(this, "monster-host");
1582
1586
 
1583
- if (!(isInstance(host, Host) && this.id)) {
1587
+ if (!(isInstance(host, Host) && hasConfigIdentity.call(this))) {
1584
1588
  return Promise.resolve();
1585
1589
  }
1586
1590
 
@@ -1622,7 +1626,7 @@ function initFromConfig() {
1622
1626
  */
1623
1627
  function updateConfig() {
1624
1628
  const host = findElementWithSelectorUpwards(this, "monster-host");
1625
- if (!(host && this.id)) {
1629
+ if (!(host && hasConfigIdentity.call(this))) {
1626
1630
  return;
1627
1631
  }
1628
1632
  const configKey = getFilterConfigKey.call(this);
@@ -1634,6 +1638,18 @@ function updateConfig() {
1634
1638
  }
1635
1639
  }
1636
1640
 
1641
+ /**
1642
+ * @private
1643
+ * @return {boolean}
1644
+ */
1645
+ function hasConfigIdentity() {
1646
+ return (
1647
+ (isString(this.id) && this.id !== "") ||
1648
+ (isString(this.getOption("config.instanceKey")) &&
1649
+ this.getOption("config.instanceKey").trim() !== "")
1650
+ );
1651
+ }
1652
+
1637
1653
  /**
1638
1654
  * @private
1639
1655
  * @return {string}
@@ -21,6 +21,7 @@ import { ConfigManagerStyleSheet } from "./stylesheet/config-manager.mjs";
21
21
  import { getWindow } from "../../dom/util.mjs";
22
22
  import { instanceSymbol } from "../../constants.mjs";
23
23
  import { diff } from "../../data/diff.mjs";
24
+ import { isFunction, isObject } from "../../types/is.mjs";
24
25
 
25
26
  export { ConfigManager };
26
27
 
@@ -108,17 +109,42 @@ class ConfigManager extends CustomElement {
108
109
  keyPath: "key",
109
110
  },
110
111
  },
112
+ storage: {
113
+ adapter: null,
114
+ serverAuthoritative: false,
115
+ },
111
116
  });
112
117
  }
113
118
 
119
+ /**
120
+ * Register an external host configuration storage adapter.
121
+ *
122
+ * Supported adapter methods are getConfig/hasConfig/setConfig/deleteConfig
123
+ * or their short aliases get/has/set/delete. If managesConfigKey, managesKey
124
+ * or manages is present, it is used to decide whether a key is handled by
125
+ * the adapter. Without such a method, an adapter handles all keys.
126
+ *
127
+ * @param {Object|null} adapter
128
+ * @return {ConfigManager}
129
+ */
130
+ setStorageAdapter(adapter) {
131
+ this.setOption("storage.adapter", adapter);
132
+ return this;
133
+ }
134
+
135
+ /**
136
+ * @return {Object|null}
137
+ */
138
+ getStorageAdapter() {
139
+ return getStorageAdapter.call(this);
140
+ }
141
+
114
142
  /**
115
143
  * @param {string} key
116
144
  * @return {Promise<unknown>}
117
145
  */
118
146
  getConfig(key) {
119
- return this.ready().then(() => {
120
- return getBlob.call(this, key);
121
- });
147
+ return this.ready().then(() => readConfig.call(this, key));
122
148
  }
123
149
 
124
150
  /**
@@ -126,16 +152,7 @@ class ConfigManager extends CustomElement {
126
152
  * @return {Promise<boolean>}
127
153
  */
128
154
  hasConfig(key) {
129
- return this.ready()
130
- .then(() => {
131
- return getBlob.call(this, key);
132
- })
133
- .then(() => {
134
- return true;
135
- })
136
- .catch(() => {
137
- return false;
138
- });
155
+ return this.ready().then(() => hasConfig.call(this, key));
139
156
  }
140
157
 
141
158
  /**
@@ -144,28 +161,11 @@ class ConfigManager extends CustomElement {
144
161
  * @return {Promise<unknown>}
145
162
  */
146
163
  setConfig(key, value) {
147
- return this.ready().then(() => {
148
- return getBlob
149
- .call(this, key)
150
- .then((storedValue) => {
151
- if (diff(storedValue, value).length === 0) {
152
- return;
153
- }
154
- return setBlob.call(this, key, value);
155
- })
156
- .catch((error) => {
157
- if (error?.message?.match(/is not defined/)) {
158
- return setBlob.call(this, key, value);
159
- }
160
- throw error;
161
- });
162
- });
164
+ return this.ready().then(() => writeConfig.call(this, key, value));
163
165
  }
164
166
 
165
167
  deleteConfig(key) {
166
- return this.ready().then(() => {
167
- return deleteBlob.call(this, key);
168
- });
168
+ return this.ready().then(() => removeConfig.call(this, key));
169
169
  }
170
170
 
171
171
  /**
@@ -190,6 +190,192 @@ class ConfigManager extends CustomElement {
190
190
  }
191
191
  }
192
192
 
193
+ /**
194
+ * @private
195
+ * @return {Object|null}
196
+ */
197
+ function getStorageAdapter() {
198
+ const adapter = this.getOption("storage.adapter");
199
+ return isObject(adapter) ? adapter : null;
200
+ }
201
+
202
+ /**
203
+ * @private
204
+ * @param {Object} adapter
205
+ * @param {string} key
206
+ * @return {boolean}
207
+ */
208
+ function adapterManagesKey(adapter, key) {
209
+ for (const methodName of ["managesConfigKey", "managesKey", "manages"]) {
210
+ if (isFunction(adapter?.[methodName])) {
211
+ return adapter[methodName](key) === true;
212
+ }
213
+ }
214
+
215
+ return true;
216
+ }
217
+
218
+ /**
219
+ * @private
220
+ * @param {Object} adapter
221
+ * @param {string[]} methodNames
222
+ * @param {Array} args
223
+ * @return {Promise<unknown>}
224
+ */
225
+ function callAdapter(adapter, methodNames, args) {
226
+ for (const methodName of methodNames) {
227
+ if (isFunction(adapter?.[methodName])) {
228
+ return Promise.resolve(adapter[methodName](...args));
229
+ }
230
+ }
231
+
232
+ return Promise.reject(new Error("The storage adapter method is not defined."));
233
+ }
234
+
235
+ /**
236
+ * @private
237
+ * @return {boolean}
238
+ */
239
+ function isServerAuthoritative() {
240
+ return this.getOption("storage.serverAuthoritative") === true;
241
+ }
242
+
243
+ /**
244
+ * @private
245
+ * @param {string} key
246
+ * @return {Promise<unknown>}
247
+ */
248
+ function readConfig(key) {
249
+ const adapter = getStorageAdapter.call(this);
250
+ const managed = adapter && adapterManagesKey(adapter, key);
251
+
252
+ if (managed) {
253
+ return callAdapter(adapter, ["getConfig", "get"], [key]).catch((error) => {
254
+ if (isServerAuthoritative.call(this)) {
255
+ throw error;
256
+ }
257
+ return getBlob.call(this, key);
258
+ });
259
+ }
260
+
261
+ return getBlob.call(this, key);
262
+ }
263
+
264
+ /**
265
+ * @private
266
+ * @param {string} key
267
+ * @return {Promise<boolean>}
268
+ */
269
+ function hasConfig(key) {
270
+ const adapter = getStorageAdapter.call(this);
271
+ const managed = adapter && adapterManagesKey(adapter, key);
272
+
273
+ if (managed) {
274
+ return callAdapter(adapter, ["hasConfig", "has"], [key])
275
+ .catch(() => {
276
+ return callAdapter(adapter, ["getConfig", "get"], [key]).then(
277
+ () => true,
278
+ );
279
+ })
280
+ .then((result) => {
281
+ if (result === true || isServerAuthoritative.call(this)) {
282
+ return result === true;
283
+ }
284
+ return hasLocalConfig.call(this, key);
285
+ })
286
+ .catch(() => {
287
+ if (isServerAuthoritative.call(this)) {
288
+ return false;
289
+ }
290
+ return hasLocalConfig.call(this, key);
291
+ });
292
+ }
293
+
294
+ return hasLocalConfig.call(this, key);
295
+ }
296
+
297
+ /**
298
+ * @private
299
+ * @param {string} key
300
+ * @return {Promise<boolean>}
301
+ */
302
+ function hasLocalConfig(key) {
303
+ return getBlob
304
+ .call(this, key)
305
+ .then(() => true)
306
+ .catch(() => false);
307
+ }
308
+
309
+ /**
310
+ * @private
311
+ * @param {string} key
312
+ * @param {*} value
313
+ * @return {Promise<unknown>}
314
+ */
315
+ function writeConfig(key, value) {
316
+ const adapter = getStorageAdapter.call(this);
317
+ const managed = adapter && adapterManagesKey(adapter, key);
318
+
319
+ if (managed) {
320
+ return callAdapter(adapter, ["setConfig", "set"], [key, value]).catch(
321
+ (error) => {
322
+ if (isServerAuthoritative.call(this)) {
323
+ throw error;
324
+ }
325
+ return writeLocalConfig.call(this, key, value);
326
+ },
327
+ );
328
+ }
329
+
330
+ return writeLocalConfig.call(this, key, value);
331
+ }
332
+
333
+ /**
334
+ * @private
335
+ * @param {string} key
336
+ * @param {*} value
337
+ * @return {Promise<unknown>}
338
+ */
339
+ function writeLocalConfig(key, value) {
340
+ return getBlob
341
+ .call(this, key)
342
+ .then((storedValue) => {
343
+ if (diff(storedValue, value).length === 0) {
344
+ return;
345
+ }
346
+ return setBlob.call(this, key, value);
347
+ })
348
+ .catch((error) => {
349
+ if (error?.message?.match(/is not defined/)) {
350
+ return setBlob.call(this, key, value);
351
+ }
352
+ throw error;
353
+ });
354
+ }
355
+
356
+ /**
357
+ * @private
358
+ * @param {string} key
359
+ * @return {Promise<unknown>}
360
+ */
361
+ function removeConfig(key) {
362
+ const adapter = getStorageAdapter.call(this);
363
+ const managed = adapter && adapterManagesKey(adapter, key);
364
+
365
+ if (managed) {
366
+ return callAdapter(adapter, ["deleteConfig", "delete", "remove"], [
367
+ key,
368
+ ]).catch((error) => {
369
+ if (isServerAuthoritative.call(this)) {
370
+ throw error;
371
+ }
372
+ return deleteBlob.call(this, key);
373
+ });
374
+ }
375
+
376
+ return deleteBlob.call(this, key);
377
+ }
378
+
193
379
  /**
194
380
  * @private
195
381
  * @returns {Promise<unknown>}
@@ -206,6 +392,10 @@ function openDatabase() {
206
392
  throw new Error("The database name and version must be set.");
207
393
  }
208
394
 
395
+ if (!window.indexedDB) {
396
+ return Promise.resolve(null);
397
+ }
398
+
209
399
  const request = window.indexedDB.open(name, version);
210
400
 
211
401
  return new Promise((resolve, reject) => {
@@ -194,6 +194,32 @@ class Host extends CustomElement {
194
194
  return this[configManagerElementSymbol].setConfig(key, value);
195
195
  }
196
196
 
197
+ /**
198
+ * Register an external storage adapter for host configuration.
199
+ *
200
+ * @param {Object|null} adapter
201
+ * @return {Host}
202
+ */
203
+ setConfigStorageAdapter(adapter) {
204
+ if (this[configManagerElementSymbol] instanceof HTMLElement === false) {
205
+ throw new Error("There is no config manager element");
206
+ }
207
+
208
+ this[configManagerElementSymbol].setStorageAdapter(adapter);
209
+ return this;
210
+ }
211
+
212
+ /**
213
+ * @return {Object|null}
214
+ */
215
+ getConfigStorageAdapter() {
216
+ if (this[configManagerElementSymbol] instanceof HTMLElement === false) {
217
+ throw new Error("There is no config manager element");
218
+ }
219
+
220
+ return this[configManagerElementSymbol].getStorageAdapter();
221
+ }
222
+
197
223
  /**
198
224
  * @private
199
225
  * @fires Host#monster-host-connected
@@ -13,8 +13,14 @@
13
13
  */
14
14
 
15
15
  import { getWindow } from "../../dom/util.mjs";
16
+ import { isString } from "../../types/is.mjs";
16
17
 
17
- export { generateUniqueConfigKey };
18
+ export {
19
+ generateConfigKey,
20
+ generateComponentConfigKey,
21
+ generateUniqueConfigKey,
22
+ sanitizeConfigKey,
23
+ };
18
24
 
19
25
  /**
20
26
  * Generate a unique configuration key based on the current browser location,
@@ -38,5 +44,49 @@ function generateUniqueConfigKey(componentName, id, prefix) {
38
44
  const uniqueKey = `${prefix}_${urlWithoutParamsAndHash}_${componentName}_${id}`;
39
45
 
40
46
  // Replace any special characters and spaces with underscores
41
- return uniqueKey.replace(/[^\w\s]/gi, "_").replace(/\s+/g, "_");
47
+ return sanitizeConfigKey(uniqueKey);
48
+ }
49
+
50
+ /**
51
+ * Generate a stable host configuration key from an explicit technical instance key.
52
+ *
53
+ * @param {string} componentName - The name of the component.
54
+ * @param {string} instanceKey - Technical, host-defined instance key.
55
+ * @param {string} scope - Configuration scope, for example "filter".
56
+ * @return {string}
57
+ */
58
+ function generateConfigKey(componentName, instanceKey, scope) {
59
+ return sanitizeConfigKey(`${scope}_${componentName}_${instanceKey}`);
60
+ }
61
+
62
+ /**
63
+ * Generate a component host configuration key.
64
+ *
65
+ * If an explicit instance key is provided, the key is independent from the
66
+ * current URL and DOM id. Without an instance key, this keeps the historical
67
+ * URL-derived key for backwards compatibility.
68
+ *
69
+ * @param {string} componentName
70
+ * @param {string} id
71
+ * @param {string} scope
72
+ * @param {object} [options]
73
+ * @param {string} [options.instanceKey]
74
+ * @return {string}
75
+ */
76
+ function generateComponentConfigKey(componentName, id, scope, options = {}) {
77
+ if (isString(options.instanceKey) && options.instanceKey.trim() !== "") {
78
+ return generateConfigKey(componentName, options.instanceKey.trim(), scope);
79
+ }
80
+
81
+ return generateUniqueConfigKey(componentName, id, scope);
82
+ }
83
+
84
+ /**
85
+ * @param {string} key
86
+ * @return {string}
87
+ */
88
+ function sanitizeConfigKey(key) {
89
+ return String(key)
90
+ .replace(/[^\w\s]/gi, "_")
91
+ .replace(/\s+/g, "_");
42
92
  }
@@ -0,0 +1,47 @@
1
+ import { expect } from "chai";
2
+ import { initJSDOM } from "../../../util/jsdom.mjs";
3
+
4
+ let getStoredOrderConfigKey;
5
+ let getFilterConfigKey;
6
+ let getStoredFilterConfigKey;
7
+
8
+ describe("Datatable config keys", function () {
9
+ before(function (done) {
10
+ initJSDOM().then(() => {
11
+ import("../../../../source/components/datatable/datatable.mjs")
12
+ .then((m) => {
13
+ getStoredOrderConfigKey = m.getStoredOrderConfigKey;
14
+ return import(
15
+ "../../../../source/components/datatable/filter/util.mjs"
16
+ );
17
+ })
18
+ .then((m) => {
19
+ getFilterConfigKey = m.getFilterConfigKey;
20
+ getStoredFilterConfigKey = m.getStoredFilterConfigKey;
21
+ done();
22
+ })
23
+ .catch(done);
24
+ });
25
+ });
26
+
27
+ it("uses the same explicit instance-key contract for filter, stored filters and sorting", function () {
28
+ const context = {
29
+ id: "dom-id",
30
+ getOption(path) {
31
+ if (path === "config.instanceKey") {
32
+ return "orders.default";
33
+ }
34
+ },
35
+ };
36
+
37
+ expect(getFilterConfigKey.call(context)).to.equal(
38
+ "filter_datatable_orders_default",
39
+ );
40
+ expect(getStoredFilterConfigKey.call(context)).to.equal(
41
+ "stored_filter_datatable_orders_default",
42
+ );
43
+ expect(getStoredOrderConfigKey.call(context)).to.equal(
44
+ "stored_order_datatable_orders_default",
45
+ );
46
+ });
47
+ });
@@ -0,0 +1,86 @@
1
+ import * as chai from "chai";
2
+ import { initJSDOM } from "../../../util/jsdom.mjs";
3
+
4
+ const expect = chai.expect;
5
+
6
+ describe("ConfigManager storage adapter", function () {
7
+ before(function (done) {
8
+ initJSDOM().then(() => {
9
+ import("../../../../source/components/host/config-manager.mjs")
10
+ .then(() => {
11
+ done();
12
+ })
13
+ .catch((e) => done(e));
14
+ });
15
+ });
16
+
17
+ afterEach(() => {
18
+ let mocks = document.getElementById("mocks");
19
+ mocks.innerHTML = "";
20
+ });
21
+
22
+ it("uses an external storage adapter for managed keys", function (done) {
23
+ const config = document.createElement("monster-config-manager");
24
+ const values = new Map();
25
+ const calls = [];
26
+
27
+ config.setStorageAdapter({
28
+ managesConfigKey: (key) => key.startsWith("server_"),
29
+ hasConfig: (key) => values.has(key),
30
+ getConfig: (key) => values.get(key),
31
+ setConfig: (key, value) => {
32
+ calls.push(["set", key, value]);
33
+ values.set(key, value);
34
+ },
35
+ deleteConfig: (key) => {
36
+ calls.push(["delete", key]);
37
+ values.delete(key);
38
+ },
39
+ });
40
+
41
+ config.setOption("storage.serverAuthoritative", true);
42
+
43
+ config
44
+ .setConfig("server_table_filter", { visible: true })
45
+ .then(() => config.hasConfig("server_table_filter"))
46
+ .then((hasConfig) => {
47
+ expect(hasConfig).to.equal(true);
48
+ return config.getConfig("server_table_filter");
49
+ })
50
+ .then((value) => {
51
+ expect(value).to.deep.equal({ visible: true });
52
+ expect(calls).to.deep.equal([
53
+ ["set", "server_table_filter", { visible: true }],
54
+ ]);
55
+ return config.deleteConfig("server_table_filter");
56
+ })
57
+ .then(() => {
58
+ expect(values.has("server_table_filter")).to.equal(false);
59
+ done();
60
+ })
61
+ .catch(done);
62
+ });
63
+
64
+ it("does not fall back to IndexedDB for server-authoritative adapter keys", function (done) {
65
+ const config = document.createElement("monster-config-manager");
66
+
67
+ config.setStorageAdapter({
68
+ managesConfigKey: (key) => key === "server_key",
69
+ setConfig: () => Promise.reject(new Error("server unavailable")),
70
+ });
71
+ config.setOption("storage.serverAuthoritative", true);
72
+ config.setOption("indexDB.objectStore.name", "missing-store");
73
+
74
+ config
75
+ .setConfig("server_key", { value: 1 })
76
+ .then(() => done(new Error("setConfig should reject")))
77
+ .catch((error) => {
78
+ try {
79
+ expect(error.message).to.equal("server unavailable");
80
+ done();
81
+ } catch (e) {
82
+ done(e);
83
+ }
84
+ });
85
+ });
86
+ });
@@ -1,79 +1,105 @@
1
1
  // Import the required libraries
2
- import { expect } from 'chai';
2
+ import { expect } from "chai";
3
3
  //import { JSDOM } from 'jsdom';
4
- import { generateUniqueConfigKey } from '../../../../source/components/host/util.mjs';
5
- import {initJSDOM} from "../../../util/jsdom.mjs";
4
+ import {
5
+ generateComponentConfigKey,
6
+ generateConfigKey,
7
+ generateUniqueConfigKey,
8
+ } from "../../../../source/components/host/util.mjs";
9
+ import { initJSDOM } from "../../../util/jsdom.mjs";
6
10
 
7
11
  // Create a JSDOM instance to simulate the browser environment
8
12
  //const dom = new JSDOM();
9
13
 
10
-
11
14
  // Test suite for the generateUniqueConfigKey function
12
- describe('generateUniqueConfigKey', () => {
13
-
14
- //let originalWindow;
15
-
16
- // before(() => {
17
- // // Store the original window object
18
- // originalWindow = globalThis.window;
19
- //
20
- // // Create a JSDOM instance to simulate the browser environment
21
- // const dom = new JSDOM();
22
- // globalThis.window = dom.window;
23
- // });
24
-
25
-
26
- before(function (done) {
27
- initJSDOM().then(() => {
28
- done();
29
- });
30
- })
31
-
32
- // ... (same test cases as before)
33
-
34
- after(() => {
35
- // Restore the original window object
36
- // globalThis.window = originalWindow;
37
- });
38
-
39
- it('should generate a unique key with the given parameters', () => {
40
- const componentName = 'MyComponent';
41
- const id = '123';
42
- const prefix = 'myPrefix';
43
-
44
- const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
45
-
46
- // Ensure the unique key contains the given parameters and follows the expected format
47
- expect(uniqueKey).to.include(prefix);
48
- expect(uniqueKey).to.include(componentName);
49
- expect(uniqueKey).to.include(id);
50
- expect(uniqueKey).to.match(/^[a-zA-Z0-9_]+$/);
51
- });
52
-
53
- it('should replace special characters and spaces with underscores', () => {
54
- const componentName = 'My$Component';
55
- const id = '12#3';
56
- const prefix = 'my Prefix';
57
-
58
- const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
59
-
60
- // Ensure the unique key does not contain any special characters or spaces
61
- expect(uniqueKey).to.match(/^[a-zA-Z0-9_]+$/);
62
- });
63
-
64
- it('should include the browser location without parameters', () => {
65
- const componentName = 'MyComponent';
66
- const id = '123';
67
- const prefix = 'myPrefix';
68
-
69
- const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
70
-
71
- // Ensure the unique key contains the browser location without parameters
72
- const urlWithoutParams = window.location.href.split('?')[0];
73
- const sanitizedUrl = urlWithoutParams.replace(/[^\w\s]/gi, '_').replace(/\s+/g, '_');
74
- expect(uniqueKey).to.include(sanitizedUrl);
75
- });
76
-
77
-
78
-
79
- })
15
+ describe("generateUniqueConfigKey", () => {
16
+ //let originalWindow;
17
+
18
+ // before(() => {
19
+ // // Store the original window object
20
+ // originalWindow = globalThis.window;
21
+ //
22
+ // // Create a JSDOM instance to simulate the browser environment
23
+ // const dom = new JSDOM();
24
+ // globalThis.window = dom.window;
25
+ // });
26
+
27
+ before(function (done) {
28
+ initJSDOM().then(() => {
29
+ done();
30
+ });
31
+ });
32
+
33
+ // ... (same test cases as before)
34
+
35
+ after(() => {
36
+ // Restore the original window object
37
+ // globalThis.window = originalWindow;
38
+ });
39
+
40
+ it("should generate a unique key with the given parameters", () => {
41
+ const componentName = "MyComponent";
42
+ const id = "123";
43
+ const prefix = "myPrefix";
44
+
45
+ const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
46
+
47
+ // Ensure the unique key contains the given parameters and follows the expected format
48
+ expect(uniqueKey).to.include(prefix);
49
+ expect(uniqueKey).to.include(componentName);
50
+ expect(uniqueKey).to.include(id);
51
+ expect(uniqueKey).to.match(/^[a-zA-Z0-9_]+$/);
52
+ });
53
+
54
+ it("should replace special characters and spaces with underscores", () => {
55
+ const componentName = "My$Component";
56
+ const id = "12#3";
57
+ const prefix = "my Prefix";
58
+
59
+ const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
60
+
61
+ // Ensure the unique key does not contain any special characters or spaces
62
+ expect(uniqueKey).to.match(/^[a-zA-Z0-9_]+$/);
63
+ });
64
+
65
+ it("should include the browser location without parameters", () => {
66
+ const componentName = "MyComponent";
67
+ const id = "123";
68
+ const prefix = "myPrefix";
69
+
70
+ const uniqueKey = generateUniqueConfigKey(componentName, id, prefix);
71
+
72
+ // Ensure the unique key contains the browser location without parameters
73
+ const urlWithoutParams = window.location.href.split("?")[0];
74
+ const sanitizedUrl = urlWithoutParams
75
+ .replace(/[^\w\s]/gi, "_")
76
+ .replace(/\s+/g, "_");
77
+ expect(uniqueKey).to.include(sanitizedUrl);
78
+ });
79
+
80
+ it("should generate explicit instance keys without the browser location", () => {
81
+ const uniqueKey = generateConfigKey(
82
+ "datatable",
83
+ "orders.default",
84
+ "filter",
85
+ );
86
+
87
+ expect(uniqueKey).to.equal("filter_datatable_orders_default");
88
+ expect(uniqueKey).to.not.include("example_test");
89
+ });
90
+
91
+ it("should prefer explicit instance keys over legacy URL keys", () => {
92
+ const uniqueKey = generateComponentConfigKey(
93
+ "datatable",
94
+ "dom-id",
95
+ "stored-order",
96
+ {
97
+ instanceKey: "orders.default",
98
+ },
99
+ );
100
+
101
+ expect(uniqueKey).to.equal("stored_order_datatable_orders_default");
102
+ expect(uniqueKey).to.not.include("dom-id");
103
+ expect(uniqueKey).to.not.include("example_test");
104
+ });
105
+ });
@@ -25,8 +25,10 @@ import "../cases/components/notify/message.mjs";
25
25
  import "../cases/components/notify/notify.mjs";
26
26
  import "../cases/components/host/host.mjs";
27
27
  import "../cases/components/host/overlay.mjs";
28
+ import "../cases/components/host/config-manager.mjs";
28
29
  import "../cases/components/host/util.mjs";
29
30
  import "../cases/components/host/details.mjs";
31
+ import "../cases/components/datatable/config-keys.mjs";
30
32
  import "../cases/components/datatable/writeback-sanitizer.mjs";
31
33
  import "../cases/components/datatable/pagination.mjs";
32
34
  import "../cases/components/navigation/site-navigation.mjs";