declarative-forms 2.1.1 → 2.1.4
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/dist/index.cjs +83 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -5
- package/dist/index.d.ts +52 -5
- package/dist/index.js +83 -15
- package/dist/index.js.map +1 -1
- package/package.json +10 -4
- package/styles/classic.css +15 -4
package/dist/index.cjs
CHANGED
|
@@ -488,7 +488,7 @@ var ArrayField = class extends Field {
|
|
|
488
488
|
buttons: {
|
|
489
489
|
OK: {
|
|
490
490
|
id: confirmId,
|
|
491
|
-
isActive: ({ data }) => this.validateEntry(data),
|
|
491
|
+
isActive: ({ data }) => this.validateEntry(data, form),
|
|
492
492
|
action: (values) => {
|
|
493
493
|
if (editing && index !== void 0) this.entries[index] = values;
|
|
494
494
|
else this.entries.push(values);
|
|
@@ -509,9 +509,21 @@ var ArrayField = class extends Field {
|
|
|
509
509
|
}));
|
|
510
510
|
return this.descriptor.mapFieldsOnEdit?.(seeded, entry) ?? seeded;
|
|
511
511
|
}
|
|
512
|
-
|
|
512
|
+
/**
|
|
513
|
+
* Run `isValidRecord` against the entry dialog.
|
|
514
|
+
*
|
|
515
|
+
* `ctx.form` is the entry dialog rather than the form owning the array field:
|
|
516
|
+
* the entry's fields — and their tooltip markers — live in the dialog, and
|
|
517
|
+
* tooltips are scoped per form, so `ctx.form.setTooltipError(…)` on the outer
|
|
518
|
+
* form was a silent no-op. The rest of the context still describes the outer
|
|
519
|
+
* field.
|
|
520
|
+
*/
|
|
521
|
+
validateEntry(entry, dialog) {
|
|
513
522
|
if (!this.descriptor.isValidRecord) return true;
|
|
514
|
-
return this.descriptor.isValidRecord(entry,
|
|
523
|
+
return this.descriptor.isValidRecord(entry, {
|
|
524
|
+
...this.context.contextFor(this.descriptor),
|
|
525
|
+
form: dialog
|
|
526
|
+
});
|
|
515
527
|
}
|
|
516
528
|
notifyChange() {
|
|
517
529
|
const onChange = this.descriptor.onChange;
|
|
@@ -1329,6 +1341,8 @@ function defineDlSelect() {
|
|
|
1329
1341
|
// src/fields/SelectField.ts
|
|
1330
1342
|
var SelectField = class extends Field {
|
|
1331
1343
|
select;
|
|
1344
|
+
/** True while the tooltip shows a message from {@link reportOptionsError}. */
|
|
1345
|
+
showsOptionsError = false;
|
|
1332
1346
|
get wrapperExtraClass() {
|
|
1333
1347
|
return "dl-select-wrapper";
|
|
1334
1348
|
}
|
|
@@ -1366,7 +1380,7 @@ var SelectField = class extends Field {
|
|
|
1366
1380
|
}
|
|
1367
1381
|
const dependsOnTrigger = triggerName !== void 0 && this.descriptor.reloadOnChangeOf?.includes(triggerName) === true;
|
|
1368
1382
|
if (dependsOnTrigger && typeof this.descriptor.options === "function") {
|
|
1369
|
-
void this.loadOptions(
|
|
1383
|
+
void this.loadOptions();
|
|
1370
1384
|
}
|
|
1371
1385
|
}
|
|
1372
1386
|
/**
|
|
@@ -1377,16 +1391,20 @@ var SelectField = class extends Field {
|
|
|
1377
1391
|
* fresher options. v1 had no such guard, so a slow response from an earlier
|
|
1378
1392
|
* keystroke could repopulate the select with outdated values.
|
|
1379
1393
|
*/
|
|
1380
|
-
async loadOptions(
|
|
1394
|
+
async loadOptions() {
|
|
1381
1395
|
const isCurrent = this.context.scheduler.claim(`options:${this.descriptor.name}`);
|
|
1382
1396
|
const source = this.descriptor.options;
|
|
1383
|
-
|
|
1384
|
-
const isAsync = requested instanceof Promise;
|
|
1385
|
-
if (isAsync) this.select.setLoadingStatus();
|
|
1397
|
+
let isAsync = false;
|
|
1386
1398
|
try {
|
|
1399
|
+
const requested = typeof source === "function" ? source(this.context.contextFor(this.descriptor)) : source;
|
|
1400
|
+
isAsync = requested instanceof Promise;
|
|
1401
|
+
if (isAsync) this.select.setLoadingStatus();
|
|
1387
1402
|
const options = await requested ?? [];
|
|
1388
1403
|
if (!isCurrent()) return;
|
|
1389
|
-
if (
|
|
1404
|
+
if (this.showsOptionsError) {
|
|
1405
|
+
this.context.tooltips.reset(this.descriptor.name);
|
|
1406
|
+
this.showsOptionsError = false;
|
|
1407
|
+
}
|
|
1390
1408
|
const previous = this.select.getValue();
|
|
1391
1409
|
this.select.removeAllOptions();
|
|
1392
1410
|
for (const option of options) this.select.addOption(this.createOption(option));
|
|
@@ -1420,6 +1438,7 @@ var SelectField = class extends Field {
|
|
|
1420
1438
|
});
|
|
1421
1439
|
if (message) {
|
|
1422
1440
|
this.context.tooltips.setMessage(this.descriptor.name, message.level, message.text);
|
|
1441
|
+
this.showsOptionsError = true;
|
|
1423
1442
|
}
|
|
1424
1443
|
}
|
|
1425
1444
|
};
|
|
@@ -1601,6 +1620,7 @@ var IdGenerator = class {
|
|
|
1601
1620
|
// src/core/UpdateScheduler.ts
|
|
1602
1621
|
var UpdateScheduler = class {
|
|
1603
1622
|
pending = /* @__PURE__ */ new Set();
|
|
1623
|
+
updates = /* @__PURE__ */ new Set();
|
|
1604
1624
|
generations = /* @__PURE__ */ new Map();
|
|
1605
1625
|
/** Register async work and drop it from the set once it settles. */
|
|
1606
1626
|
track(work) {
|
|
@@ -1611,25 +1631,64 @@ var UpdateScheduler = class {
|
|
|
1611
1631
|
work.then(forget, forget);
|
|
1612
1632
|
return work;
|
|
1613
1633
|
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Register an `update()` pass.
|
|
1636
|
+
*
|
|
1637
|
+
* Update passes are tracked separately from field work because `update()`
|
|
1638
|
+
* waits for field work *as part of* running, so a single set would make it
|
|
1639
|
+
* wait for itself. `whenSettled` drains both; `whenFieldWorkSettled`, which
|
|
1640
|
+
* is what `update()` itself calls, drains only `pending`.
|
|
1641
|
+
*
|
|
1642
|
+
* Tracking these at all is the point: `requestUpdate` fires an update and
|
|
1643
|
+
* returns, so before this existed `whenSettled()` could resolve with an
|
|
1644
|
+
* update still running and the button bar not yet refreshed. `ButtonBar`
|
|
1645
|
+
* awaits `whenSettled()` and then re-reads the primary button's `disabled`
|
|
1646
|
+
* class to decide whether to accept a confirm -- against a stale class, that
|
|
1647
|
+
* check passes and an invalid record is confirmed.
|
|
1648
|
+
*/
|
|
1649
|
+
trackUpdate(work) {
|
|
1650
|
+
this.updates.add(work);
|
|
1651
|
+
const forget = () => {
|
|
1652
|
+
this.updates.delete(work);
|
|
1653
|
+
};
|
|
1654
|
+
work.then(forget, forget);
|
|
1655
|
+
return work;
|
|
1656
|
+
}
|
|
1614
1657
|
/** Number of currently in-flight operations. Test/diagnostics only. */
|
|
1615
1658
|
get pendingCount() {
|
|
1616
|
-
return this.pending.size;
|
|
1659
|
+
return this.pending.size + this.updates.size;
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Resolve once nothing is in flight: no field work, and no update pass that
|
|
1663
|
+
* might still be about to act on it.
|
|
1664
|
+
*/
|
|
1665
|
+
async whenSettled() {
|
|
1666
|
+
await this.drain(() => [...this.pending, ...this.updates]);
|
|
1617
1667
|
}
|
|
1618
1668
|
/**
|
|
1619
|
-
* Resolve once
|
|
1669
|
+
* Resolve once field work is done, ignoring update passes.
|
|
1620
1670
|
*
|
|
1671
|
+
* For `update()`'s own use. It cannot wait on `whenSettled`, because it is
|
|
1672
|
+
* itself one of the update passes that would be waited for.
|
|
1673
|
+
*/
|
|
1674
|
+
async whenFieldWorkSettled() {
|
|
1675
|
+
await this.drain(() => [...this.pending]);
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1621
1678
|
* Loops because settling one batch may schedule another (an options load
|
|
1622
1679
|
* whose completion triggers a dependent field's reload).
|
|
1623
1680
|
*/
|
|
1624
|
-
async
|
|
1681
|
+
async drain(snapshot) {
|
|
1625
1682
|
let guard = 0;
|
|
1626
|
-
|
|
1683
|
+
let outstanding = snapshot();
|
|
1684
|
+
while (outstanding.length > 0) {
|
|
1627
1685
|
if (++guard > 100) {
|
|
1628
1686
|
throw new Error(
|
|
1629
1687
|
"declarative-forms: form updates did not settle after 100 rounds \u2014 a field callback is most likely scheduling work on every update."
|
|
1630
1688
|
);
|
|
1631
1689
|
}
|
|
1632
|
-
await Promise.allSettled(
|
|
1690
|
+
await Promise.allSettled(outstanding);
|
|
1691
|
+
outstanding = snapshot();
|
|
1633
1692
|
}
|
|
1634
1693
|
}
|
|
1635
1694
|
/**
|
|
@@ -1650,6 +1709,7 @@ var UpdateScheduler = class {
|
|
|
1650
1709
|
/** Invalidate every outstanding claim and forget all pending work. */
|
|
1651
1710
|
reset() {
|
|
1652
1711
|
this.pending.clear();
|
|
1712
|
+
this.updates.clear();
|
|
1653
1713
|
for (const key of this.generations.keys()) {
|
|
1654
1714
|
this.generations.set(key, (this.generations.get(key) ?? 0) + 1);
|
|
1655
1715
|
}
|
|
@@ -1724,16 +1784,20 @@ var ButtonBar = class {
|
|
|
1724
1784
|
for (const { descriptor, element } of this.buttons) {
|
|
1725
1785
|
if (!descriptor.id) continue;
|
|
1726
1786
|
if (descriptor.isActive) {
|
|
1787
|
+
const isCurrent = this.callbacks.claim(`button:isActive:${descriptor.id}`);
|
|
1727
1788
|
work.push(
|
|
1728
1789
|
this.applyPredicate(descriptor.isActive(ctx), (active) => {
|
|
1790
|
+
if (!isCurrent()) return;
|
|
1729
1791
|
element.classList.toggle("disabled", !active);
|
|
1730
1792
|
element.toggleAttribute("disabled", !active);
|
|
1731
1793
|
})
|
|
1732
1794
|
);
|
|
1733
1795
|
}
|
|
1734
1796
|
if (descriptor.isVisible) {
|
|
1797
|
+
const isCurrent = this.callbacks.claim(`button:isVisible:${descriptor.id}`);
|
|
1735
1798
|
work.push(
|
|
1736
1799
|
this.applyPredicate(descriptor.isVisible(ctx), (visible) => {
|
|
1800
|
+
if (!isCurrent()) return;
|
|
1737
1801
|
element.classList.toggle("invisible", !visible);
|
|
1738
1802
|
})
|
|
1739
1803
|
);
|
|
@@ -2130,6 +2194,7 @@ var DeclarativeForm = class _DeclarativeForm {
|
|
|
2130
2194
|
this.tabBar = new TabBar((tab) => this.setActiveTab(tab));
|
|
2131
2195
|
this.buttonBar = new ButtonBar(this.resolveButtons(), {
|
|
2132
2196
|
whenSettled: () => this.scheduler.whenSettled(),
|
|
2197
|
+
claim: (key) => this.scheduler.claim(key),
|
|
2133
2198
|
updateComputedFields: () => this.updateComputedFields(),
|
|
2134
2199
|
getValues: () => this.getValues(),
|
|
2135
2200
|
closeWith: (action) => this.close(action)
|
|
@@ -2326,6 +2391,9 @@ var DeclarativeForm = class _DeclarativeForm {
|
|
|
2326
2391
|
* @param includeTab treat a tab switch as a change
|
|
2327
2392
|
*/
|
|
2328
2393
|
async update(source, force = false, includeTab = false) {
|
|
2394
|
+
return this.scheduler.trackUpdate(this.runUpdate(source, force, includeTab));
|
|
2395
|
+
}
|
|
2396
|
+
async runUpdate(source, force = false, includeTab = false) {
|
|
2329
2397
|
const values = this.getValues();
|
|
2330
2398
|
const changed = this.model.hasChanged(values, includeTab);
|
|
2331
2399
|
if (!changed && !force) return;
|
|
@@ -2349,7 +2417,7 @@ var DeclarativeForm = class _DeclarativeForm {
|
|
|
2349
2417
|
field.onFormUpdate(ctx, triggerName);
|
|
2350
2418
|
}
|
|
2351
2419
|
this.renderTabs(activeTabs);
|
|
2352
|
-
await this.scheduler.
|
|
2420
|
+
await this.scheduler.whenFieldWorkSettled();
|
|
2353
2421
|
if (changed) this.model.notify(this.getValues());
|
|
2354
2422
|
await this.buttonBar.refresh(this.buttonContext());
|
|
2355
2423
|
}
|