@schukai/monster 4.148.5 → 4.148.6
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.148.
|
|
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.148.6"}
|
|
@@ -75,8 +75,9 @@ const originValuesSymbol = Symbol.for(
|
|
|
75
75
|
*/
|
|
76
76
|
const badgeElementSymbol = Symbol("badgeElement");
|
|
77
77
|
const saveInFlightSymbol = Symbol("saveInFlight");
|
|
78
|
-
const pendingResetSymbol = Symbol("pendingReset");
|
|
79
78
|
const fetchInFlightSymbol = Symbol("fetchInFlight");
|
|
79
|
+
const readRevisionSymbol = Symbol("readRevision");
|
|
80
|
+
const pendingWriteSymbol = Symbol("pendingWrite");
|
|
80
81
|
const originInitializedSymbol = Symbol("originInitialized");
|
|
81
82
|
const internalDisableTimeoutSymbol = Symbol("internalDisableTimeout");
|
|
82
83
|
const internalDisableVersionSymbol = Symbol("internalDisableVersion");
|
|
@@ -206,33 +207,84 @@ class SaveButton extends CustomElement {
|
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
if (element instanceof RestDatasource) {
|
|
209
|
-
element.addEventListener(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
210
|
+
element.addEventListener(
|
|
211
|
+
"monster-datasource-fetch",
|
|
212
|
+
(event) => {
|
|
213
|
+
if (
|
|
214
|
+
event?.detail?.operation ===
|
|
215
|
+
DATASOURCE_OPERATION_WRITE
|
|
216
|
+
) {
|
|
217
|
+
self[pendingWriteSymbol] = {
|
|
218
|
+
data: clone(
|
|
219
|
+
self[datasourceLinkedElementSymbol].data,
|
|
220
|
+
),
|
|
221
|
+
readRevision: self[readRevisionSymbol] || 0,
|
|
222
|
+
};
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
self[fetchInFlightSymbol] = true;
|
|
226
|
+
clearOriginValues.call(self);
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
element.addEventListener(
|
|
230
|
+
"monster-datasource-fetched",
|
|
231
|
+
(event) => {
|
|
232
|
+
if (
|
|
233
|
+
event?.detail?.operation ===
|
|
234
|
+
DATASOURCE_OPERATION_WRITE
|
|
235
|
+
) {
|
|
236
|
+
const pendingWrite = self[pendingWriteSymbol];
|
|
237
|
+
if (self[fetchInFlightSymbol] === true) {
|
|
238
|
+
if (pendingWrite) {
|
|
239
|
+
pendingWrite.completed = true;
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
self[pendingWriteSymbol] = null;
|
|
244
|
+
if (
|
|
245
|
+
pendingWrite &&
|
|
246
|
+
pendingWrite.readRevision ===
|
|
247
|
+
(self[readRevisionSymbol] || 0)
|
|
248
|
+
) {
|
|
249
|
+
setOriginValues.call(self, pendingWrite.data);
|
|
250
|
+
updateChangesState.call(self);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
222
254
|
self[fetchInFlightSymbol] = false;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
self
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
255
|
+
self[readRevisionSymbol] =
|
|
256
|
+
(self[readRevisionSymbol] || 0) + 1;
|
|
257
|
+
self[pendingWriteSymbol] = null;
|
|
258
|
+
setOriginValues.call(
|
|
259
|
+
self,
|
|
260
|
+
clone(self[datasourceLinkedElementSymbol].data),
|
|
261
|
+
);
|
|
262
|
+
updateChangesState.call(self);
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
element.addEventListener(
|
|
266
|
+
"monster-datasource-error",
|
|
267
|
+
(event) => {
|
|
268
|
+
if (
|
|
269
|
+
event?.detail?.operation ===
|
|
270
|
+
DATASOURCE_OPERATION_WRITE
|
|
271
|
+
) {
|
|
272
|
+
self[pendingWriteSymbol] = null;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
self[fetchInFlightSymbol] = false;
|
|
276
|
+
const pendingWrite = self[pendingWriteSymbol];
|
|
277
|
+
if (
|
|
278
|
+
pendingWrite?.completed === true &&
|
|
279
|
+
pendingWrite.readRevision ===
|
|
280
|
+
(self[readRevisionSymbol] || 0)
|
|
281
|
+
) {
|
|
282
|
+
self[pendingWriteSymbol] = null;
|
|
283
|
+
setOriginValues.call(self, pendingWrite.data);
|
|
284
|
+
updateChangesState.call(self);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
);
|
|
236
288
|
}
|
|
237
289
|
|
|
238
290
|
this[datasourceLinkedElementSymbol] = element;
|
|
@@ -427,35 +479,15 @@ function initEventHandler() {
|
|
|
427
479
|
.call(this)
|
|
428
480
|
.then(() => this[datasourceLinkedElementSymbol].write())
|
|
429
481
|
.then(() => {
|
|
430
|
-
|
|
431
|
-
setOriginValues.call(
|
|
432
|
-
this,
|
|
433
|
-
clone(this[datasourceLinkedElementSymbol].data),
|
|
434
|
-
);
|
|
435
|
-
this[stateButtonElementSymbol].removeState();
|
|
436
|
-
this[stateButtonElementSymbol].setOption(
|
|
437
|
-
"disabled",
|
|
438
|
-
shouldDisableWhenNoChanges.call(this),
|
|
439
|
-
);
|
|
440
|
-
this.setOption("changes", "");
|
|
441
|
-
this.setOption(
|
|
442
|
-
"classes.badge",
|
|
443
|
-
new TokenList(this.getOption("classes.badge"))
|
|
444
|
-
.add("hidden")
|
|
445
|
-
.toString(),
|
|
446
|
-
);
|
|
482
|
+
updateChangesState.call(this);
|
|
447
483
|
})
|
|
448
484
|
.catch((error) => {
|
|
449
485
|
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
|
|
450
|
-
|
|
486
|
+
updateChangesState.call(this);
|
|
451
487
|
})
|
|
452
488
|
.finally(() => {
|
|
453
489
|
this[saveInFlightSymbol] = false;
|
|
454
490
|
clearInternalDisableTimeout.call(this);
|
|
455
|
-
if (this[pendingResetSymbol]) {
|
|
456
|
-
this[pendingResetSymbol] = false;
|
|
457
|
-
clearOriginValues.call(this);
|
|
458
|
-
}
|
|
459
491
|
});
|
|
460
492
|
});
|
|
461
493
|
});
|
|
@@ -59,6 +59,16 @@ function clickSaveButton(button) {
|
|
|
59
59
|
action();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function deferred() {
|
|
63
|
+
let resolve;
|
|
64
|
+
let reject;
|
|
65
|
+
const promise = new Promise((promiseResolve, promiseReject) => {
|
|
66
|
+
resolve = promiseResolve;
|
|
67
|
+
reject = promiseReject;
|
|
68
|
+
});
|
|
69
|
+
return { promise, reject, resolve };
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
function expectPendingChanges(button) {
|
|
63
73
|
expect(Number(button.getOption("changes"))).to.be.greaterThan(0);
|
|
64
74
|
expect(getStateButton(button)?.getOption("disabled")).to.equal(false);
|
|
@@ -215,6 +225,34 @@ describe("REST datasource lifecycle", function () {
|
|
|
215
225
|
expectNoPendingChanges(second);
|
|
216
226
|
});
|
|
217
227
|
|
|
228
|
+
it("keeps edits made during a successful write pending", async function () {
|
|
229
|
+
const datasource = createDatasource("write-snapshot-datasource");
|
|
230
|
+
const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
|
|
231
|
+
const written = { data: [{ id: "area-1", name: "Written change" }] };
|
|
232
|
+
const editedDuringWrite = {
|
|
233
|
+
data: [{ id: "area-1", name: "Edited during write" }],
|
|
234
|
+
};
|
|
235
|
+
const button = createSaveButton("#write-snapshot-datasource");
|
|
236
|
+
document.getElementById("mocks").append(datasource, button);
|
|
237
|
+
await loadDatasource(datasource, loaded);
|
|
238
|
+
|
|
239
|
+
datasource.data = written;
|
|
240
|
+
await waitForUpdates();
|
|
241
|
+
expectPendingChanges(button);
|
|
242
|
+
|
|
243
|
+
const writeResult = deferred();
|
|
244
|
+
datasource.datasource.write = () => writeResult.promise;
|
|
245
|
+
clickSaveButton(button);
|
|
246
|
+
await waitForUpdates();
|
|
247
|
+
|
|
248
|
+
datasource.data = editedDuringWrite;
|
|
249
|
+
await waitForUpdates();
|
|
250
|
+
writeResult.resolve({ ok: true });
|
|
251
|
+
await waitForUpdates();
|
|
252
|
+
|
|
253
|
+
expectPendingChanges(button);
|
|
254
|
+
});
|
|
255
|
+
|
|
218
256
|
it("replaces every save button baseline after a read", async function () {
|
|
219
257
|
const datasource = createDatasource("read-baseline-datasource");
|
|
220
258
|
const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
|
|
@@ -296,4 +334,141 @@ describe("REST datasource lifecycle", function () {
|
|
|
296
334
|
expectNoPendingChanges(first);
|
|
297
335
|
expectNoPendingChanges(second);
|
|
298
336
|
});
|
|
337
|
+
|
|
338
|
+
it("preserves a read baseline when the save write finishes later", async function () {
|
|
339
|
+
const datasource = createDatasource("read-first-datasource");
|
|
340
|
+
const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
|
|
341
|
+
const written = { data: [{ id: "area-1", name: "Written change" }] };
|
|
342
|
+
const refreshed = { data: [{ id: "area-1", name: "Server refresh" }] };
|
|
343
|
+
const editedAfterRead = {
|
|
344
|
+
data: [{ id: "area-1", name: "Edited after refresh" }],
|
|
345
|
+
};
|
|
346
|
+
const button = createSaveButton("#read-first-datasource");
|
|
347
|
+
document.getElementById("mocks").append(datasource, button);
|
|
348
|
+
await loadDatasource(datasource, loaded);
|
|
349
|
+
|
|
350
|
+
datasource.data = written;
|
|
351
|
+
await waitForUpdates();
|
|
352
|
+
expectPendingChanges(button);
|
|
353
|
+
|
|
354
|
+
const writeResult = deferred();
|
|
355
|
+
datasource.datasource.write = () => writeResult.promise;
|
|
356
|
+
clickSaveButton(button);
|
|
357
|
+
await waitForUpdates();
|
|
358
|
+
|
|
359
|
+
const readResult = deferred();
|
|
360
|
+
datasource.datasource.read = () => readResult.promise;
|
|
361
|
+
const read = datasource.read();
|
|
362
|
+
await waitForUpdates();
|
|
363
|
+
datasource.data = refreshed;
|
|
364
|
+
readResult.resolve({ ok: true });
|
|
365
|
+
await read;
|
|
366
|
+
await waitForUpdates();
|
|
367
|
+
expectNoPendingChanges(button);
|
|
368
|
+
|
|
369
|
+
datasource.data = editedAfterRead;
|
|
370
|
+
await waitForUpdates();
|
|
371
|
+
expectPendingChanges(button);
|
|
372
|
+
|
|
373
|
+
writeResult.resolve({ ok: true });
|
|
374
|
+
await waitForUpdates();
|
|
375
|
+
expectPendingChanges(button);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("establishes the read baseline when the save write finishes first", async function () {
|
|
379
|
+
const datasource = createDatasource("write-first-datasource");
|
|
380
|
+
const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
|
|
381
|
+
const written = { data: [{ id: "area-1", name: "Written change" }] };
|
|
382
|
+
const refreshed = { data: [{ id: "area-1", name: "Server refresh" }] };
|
|
383
|
+
const editedAfterRead = {
|
|
384
|
+
data: [{ id: "area-1", name: "Edited after refresh" }],
|
|
385
|
+
};
|
|
386
|
+
const button = createSaveButton("#write-first-datasource");
|
|
387
|
+
document.getElementById("mocks").append(datasource, button);
|
|
388
|
+
await loadDatasource(datasource, loaded);
|
|
389
|
+
|
|
390
|
+
datasource.data = written;
|
|
391
|
+
await waitForUpdates();
|
|
392
|
+
expectPendingChanges(button);
|
|
393
|
+
|
|
394
|
+
const writeResult = deferred();
|
|
395
|
+
datasource.datasource.write = () => writeResult.promise;
|
|
396
|
+
clickSaveButton(button);
|
|
397
|
+
await waitForUpdates();
|
|
398
|
+
|
|
399
|
+
const readResult = deferred();
|
|
400
|
+
datasource.datasource.read = () => readResult.promise;
|
|
401
|
+
const read = datasource.read();
|
|
402
|
+
await waitForUpdates();
|
|
403
|
+
writeResult.resolve({ ok: true });
|
|
404
|
+
await waitForUpdates();
|
|
405
|
+
expectNoPendingChanges(button);
|
|
406
|
+
|
|
407
|
+
datasource.data = refreshed;
|
|
408
|
+
readResult.resolve({ ok: true });
|
|
409
|
+
await read;
|
|
410
|
+
await waitForUpdates();
|
|
411
|
+
expectNoPendingChanges(button);
|
|
412
|
+
|
|
413
|
+
datasource.data = editedAfterRead;
|
|
414
|
+
await waitForUpdates();
|
|
415
|
+
expectPendingChanges(button);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("falls back to the written snapshot when a concurrent read fails", async function () {
|
|
419
|
+
const datasource = createDatasource("failed-read-datasource");
|
|
420
|
+
const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
|
|
421
|
+
const written = { data: [{ id: "area-1", name: "Written change" }] };
|
|
422
|
+
const editedAfterFailure = {
|
|
423
|
+
data: [{ id: "area-1", name: "Edited after failed read" }],
|
|
424
|
+
};
|
|
425
|
+
const button = createSaveButton("#failed-read-datasource");
|
|
426
|
+
document.getElementById("mocks").append(datasource, button);
|
|
427
|
+
await loadDatasource(datasource, loaded);
|
|
428
|
+
|
|
429
|
+
datasource.data = written;
|
|
430
|
+
await waitForUpdates();
|
|
431
|
+
const writeResult = deferred();
|
|
432
|
+
datasource.datasource.write = () => writeResult.promise;
|
|
433
|
+
clickSaveButton(button);
|
|
434
|
+
await waitForUpdates();
|
|
435
|
+
|
|
436
|
+
const readResult = deferred();
|
|
437
|
+
datasource.datasource.read = () => readResult.promise;
|
|
438
|
+
const read = datasource.read().catch(() => {});
|
|
439
|
+
await waitForUpdates();
|
|
440
|
+
writeResult.resolve({ ok: true });
|
|
441
|
+
await waitForUpdates();
|
|
442
|
+
readResult.reject(new Error("Rejected concurrent read"));
|
|
443
|
+
await read;
|
|
444
|
+
await waitForUpdates();
|
|
445
|
+
expectNoPendingChanges(button);
|
|
446
|
+
|
|
447
|
+
datasource.data = editedAfterFailure;
|
|
448
|
+
await waitForUpdates();
|
|
449
|
+
expectPendingChanges(button);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("treats lifecycle events without an operation as read events", async function () {
|
|
453
|
+
const datasource = createDatasource("legacy-lifecycle-datasource");
|
|
454
|
+
const loaded = { data: [{ id: "area-1", name: "Inbound" }] };
|
|
455
|
+
const refreshed = { data: [{ id: "area-1", name: "Server refresh" }] };
|
|
456
|
+
const edited = { data: [{ id: "area-1", name: "Edited refresh" }] };
|
|
457
|
+
const button = createSaveButton("#legacy-lifecycle-datasource");
|
|
458
|
+
document.getElementById("mocks").append(datasource, button);
|
|
459
|
+
await loadDatasource(datasource, loaded);
|
|
460
|
+
|
|
461
|
+
datasource.dispatchEvent(new CustomEvent("monster-datasource-fetch"));
|
|
462
|
+
datasource.data = refreshed;
|
|
463
|
+
await waitForUpdates();
|
|
464
|
+
expectNoPendingChanges(button);
|
|
465
|
+
|
|
466
|
+
datasource.dispatchEvent(new CustomEvent("monster-datasource-fetched"));
|
|
467
|
+
await waitForUpdates();
|
|
468
|
+
expectNoPendingChanges(button);
|
|
469
|
+
|
|
470
|
+
datasource.data = edited;
|
|
471
|
+
await waitForUpdates();
|
|
472
|
+
expectPendingChanges(button);
|
|
473
|
+
});
|
|
299
474
|
});
|
|
@@ -9,6 +9,29 @@ chai.use(chaiDom);
|
|
|
9
9
|
const waitForLayout = (timeout = 120) =>
|
|
10
10
|
new Promise((resolve) => setTimeout(resolve, timeout));
|
|
11
11
|
|
|
12
|
+
function waitForCondition(check, { timeout = 4000, interval = 25 } = {}) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const start = Date.now();
|
|
15
|
+
const poll = () => {
|
|
16
|
+
try {
|
|
17
|
+
if (check()) {
|
|
18
|
+
resolve();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
|
|
23
|
+
if (Date.now() - start >= timeout) {
|
|
24
|
+
reject(new Error("Timed out waiting for condition"));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setTimeout(poll, interval);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
poll();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
const html = `
|
|
13
36
|
<div id="test1">
|
|
14
37
|
<monster-control-bar id="bar">
|
|
@@ -115,7 +138,9 @@ describe("ControlBar", function () {
|
|
|
115
138
|
await waitForLayout();
|
|
116
139
|
|
|
117
140
|
bar.setOption("layout.hideWhenEmpty", true);
|
|
118
|
-
await
|
|
141
|
+
await waitForCondition(() =>
|
|
142
|
+
bar.hasAttribute("data-monster-empty-hidden"),
|
|
143
|
+
);
|
|
119
144
|
|
|
120
145
|
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
121
146
|
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.true;
|
|
@@ -131,7 +156,9 @@ describe("ControlBar", function () {
|
|
|
131
156
|
`;
|
|
132
157
|
const bar = document.getElementById("empty-auto-hidden-bar");
|
|
133
158
|
|
|
134
|
-
await
|
|
159
|
+
await waitForCondition(() =>
|
|
160
|
+
bar.hasAttribute("data-monster-empty-hidden"),
|
|
161
|
+
);
|
|
135
162
|
|
|
136
163
|
expect(bar.getOption("layout.hideWhenEmpty")).to.equal(true);
|
|
137
164
|
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
@@ -151,7 +178,9 @@ describe("ControlBar", function () {
|
|
|
151
178
|
const bar = document.getElementById("dynamic-auto-hidden-bar");
|
|
152
179
|
const button = document.getElementById("dynamic-auto-hidden-button");
|
|
153
180
|
|
|
154
|
-
await
|
|
181
|
+
await waitForCondition(() =>
|
|
182
|
+
bar.hasAttribute("data-monster-empty-hidden"),
|
|
183
|
+
);
|
|
155
184
|
|
|
156
185
|
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
157
186
|
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.true;
|
|
@@ -179,12 +208,16 @@ describe("ControlBar", function () {
|
|
|
179
208
|
`;
|
|
180
209
|
const bar = document.getElementById("option-auto-hidden-bar");
|
|
181
210
|
|
|
182
|
-
await
|
|
211
|
+
await waitForCondition(() =>
|
|
212
|
+
bar.hasAttribute("data-monster-empty-hidden"),
|
|
213
|
+
);
|
|
183
214
|
|
|
184
215
|
expect(bar.hasAttribute("hidden")).to.be.true;
|
|
185
216
|
|
|
186
217
|
bar.setOption("layout.hideWhenEmpty", false);
|
|
187
|
-
await
|
|
218
|
+
await waitForCondition(
|
|
219
|
+
() => !bar.hasAttribute("data-monster-empty-hidden"),
|
|
220
|
+
);
|
|
188
221
|
|
|
189
222
|
expect(bar.hasAttribute("hidden")).to.be.false;
|
|
190
223
|
expect(bar.hasAttribute("data-monster-empty-hidden")).to.be.false;
|