declarative-forms 2.1.2 → 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 +79 -12
- 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 +79 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,7 +1391,7 @@ 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
1397
|
let isAsync = false;
|
|
@@ -1387,7 +1401,10 @@ var SelectField = class extends Field {
|
|
|
1387
1401
|
if (isAsync) this.select.setLoadingStatus();
|
|
1388
1402
|
const options = await requested ?? [];
|
|
1389
1403
|
if (!isCurrent()) return;
|
|
1390
|
-
if (
|
|
1404
|
+
if (this.showsOptionsError) {
|
|
1405
|
+
this.context.tooltips.reset(this.descriptor.name);
|
|
1406
|
+
this.showsOptionsError = false;
|
|
1407
|
+
}
|
|
1391
1408
|
const previous = this.select.getValue();
|
|
1392
1409
|
this.select.removeAllOptions();
|
|
1393
1410
|
for (const option of options) this.select.addOption(this.createOption(option));
|
|
@@ -1421,6 +1438,7 @@ var SelectField = class extends Field {
|
|
|
1421
1438
|
});
|
|
1422
1439
|
if (message) {
|
|
1423
1440
|
this.context.tooltips.setMessage(this.descriptor.name, message.level, message.text);
|
|
1441
|
+
this.showsOptionsError = true;
|
|
1424
1442
|
}
|
|
1425
1443
|
}
|
|
1426
1444
|
};
|
|
@@ -1602,6 +1620,7 @@ var IdGenerator = class {
|
|
|
1602
1620
|
// src/core/UpdateScheduler.ts
|
|
1603
1621
|
var UpdateScheduler = class {
|
|
1604
1622
|
pending = /* @__PURE__ */ new Set();
|
|
1623
|
+
updates = /* @__PURE__ */ new Set();
|
|
1605
1624
|
generations = /* @__PURE__ */ new Map();
|
|
1606
1625
|
/** Register async work and drop it from the set once it settles. */
|
|
1607
1626
|
track(work) {
|
|
@@ -1612,25 +1631,64 @@ var UpdateScheduler = class {
|
|
|
1612
1631
|
work.then(forget, forget);
|
|
1613
1632
|
return work;
|
|
1614
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
|
+
}
|
|
1615
1657
|
/** Number of currently in-flight operations. Test/diagnostics only. */
|
|
1616
1658
|
get pendingCount() {
|
|
1617
|
-
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]);
|
|
1618
1667
|
}
|
|
1619
1668
|
/**
|
|
1620
|
-
* Resolve once
|
|
1669
|
+
* Resolve once field work is done, ignoring update passes.
|
|
1621
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
|
+
/**
|
|
1622
1678
|
* Loops because settling one batch may schedule another (an options load
|
|
1623
1679
|
* whose completion triggers a dependent field's reload).
|
|
1624
1680
|
*/
|
|
1625
|
-
async
|
|
1681
|
+
async drain(snapshot) {
|
|
1626
1682
|
let guard = 0;
|
|
1627
|
-
|
|
1683
|
+
let outstanding = snapshot();
|
|
1684
|
+
while (outstanding.length > 0) {
|
|
1628
1685
|
if (++guard > 100) {
|
|
1629
1686
|
throw new Error(
|
|
1630
1687
|
"declarative-forms: form updates did not settle after 100 rounds \u2014 a field callback is most likely scheduling work on every update."
|
|
1631
1688
|
);
|
|
1632
1689
|
}
|
|
1633
|
-
await Promise.allSettled(
|
|
1690
|
+
await Promise.allSettled(outstanding);
|
|
1691
|
+
outstanding = snapshot();
|
|
1634
1692
|
}
|
|
1635
1693
|
}
|
|
1636
1694
|
/**
|
|
@@ -1651,6 +1709,7 @@ var UpdateScheduler = class {
|
|
|
1651
1709
|
/** Invalidate every outstanding claim and forget all pending work. */
|
|
1652
1710
|
reset() {
|
|
1653
1711
|
this.pending.clear();
|
|
1712
|
+
this.updates.clear();
|
|
1654
1713
|
for (const key of this.generations.keys()) {
|
|
1655
1714
|
this.generations.set(key, (this.generations.get(key) ?? 0) + 1);
|
|
1656
1715
|
}
|
|
@@ -1725,16 +1784,20 @@ var ButtonBar = class {
|
|
|
1725
1784
|
for (const { descriptor, element } of this.buttons) {
|
|
1726
1785
|
if (!descriptor.id) continue;
|
|
1727
1786
|
if (descriptor.isActive) {
|
|
1787
|
+
const isCurrent = this.callbacks.claim(`button:isActive:${descriptor.id}`);
|
|
1728
1788
|
work.push(
|
|
1729
1789
|
this.applyPredicate(descriptor.isActive(ctx), (active) => {
|
|
1790
|
+
if (!isCurrent()) return;
|
|
1730
1791
|
element.classList.toggle("disabled", !active);
|
|
1731
1792
|
element.toggleAttribute("disabled", !active);
|
|
1732
1793
|
})
|
|
1733
1794
|
);
|
|
1734
1795
|
}
|
|
1735
1796
|
if (descriptor.isVisible) {
|
|
1797
|
+
const isCurrent = this.callbacks.claim(`button:isVisible:${descriptor.id}`);
|
|
1736
1798
|
work.push(
|
|
1737
1799
|
this.applyPredicate(descriptor.isVisible(ctx), (visible) => {
|
|
1800
|
+
if (!isCurrent()) return;
|
|
1738
1801
|
element.classList.toggle("invisible", !visible);
|
|
1739
1802
|
})
|
|
1740
1803
|
);
|
|
@@ -2131,6 +2194,7 @@ var DeclarativeForm = class _DeclarativeForm {
|
|
|
2131
2194
|
this.tabBar = new TabBar((tab) => this.setActiveTab(tab));
|
|
2132
2195
|
this.buttonBar = new ButtonBar(this.resolveButtons(), {
|
|
2133
2196
|
whenSettled: () => this.scheduler.whenSettled(),
|
|
2197
|
+
claim: (key) => this.scheduler.claim(key),
|
|
2134
2198
|
updateComputedFields: () => this.updateComputedFields(),
|
|
2135
2199
|
getValues: () => this.getValues(),
|
|
2136
2200
|
closeWith: (action) => this.close(action)
|
|
@@ -2327,6 +2391,9 @@ var DeclarativeForm = class _DeclarativeForm {
|
|
|
2327
2391
|
* @param includeTab treat a tab switch as a change
|
|
2328
2392
|
*/
|
|
2329
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) {
|
|
2330
2397
|
const values = this.getValues();
|
|
2331
2398
|
const changed = this.model.hasChanged(values, includeTab);
|
|
2332
2399
|
if (!changed && !force) return;
|
|
@@ -2350,7 +2417,7 @@ var DeclarativeForm = class _DeclarativeForm {
|
|
|
2350
2417
|
field.onFormUpdate(ctx, triggerName);
|
|
2351
2418
|
}
|
|
2352
2419
|
this.renderTabs(activeTabs);
|
|
2353
|
-
await this.scheduler.
|
|
2420
|
+
await this.scheduler.whenFieldWorkSettled();
|
|
2354
2421
|
if (changed) this.model.notify(this.getValues());
|
|
2355
2422
|
await this.buttonBar.refresh(this.buttonContext());
|
|
2356
2423
|
}
|