@fieldia/core 0.23.0 → 0.24.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.
Files changed (177) hide show
  1. package/README.md +135 -3
  2. package/package.json +1 -1
  3. package/schema/page.schema.json +159 -4
  4. package/src/index.d.mts +6 -2
  5. package/src/index.d.ts +6 -2
  6. package/src/index.js +18 -1
  7. package/src/index.js.map +1 -1
  8. package/src/index.mjs +8 -2
  9. package/src/index.mjs.map +1 -1
  10. package/src/lib/format/autofill-group.d.mts +12 -0
  11. package/src/lib/format/autofill-group.d.ts +12 -0
  12. package/src/lib/format/autofill-group.js +23 -0
  13. package/src/lib/format/autofill-group.js.map +1 -0
  14. package/src/lib/format/autofill-group.mjs +19 -0
  15. package/src/lib/format/autofill-group.mjs.map +1 -0
  16. package/src/lib/format/autofill.d.mts +14 -0
  17. package/src/lib/format/autofill.d.ts +14 -0
  18. package/src/lib/format/autofill.js +54 -0
  19. package/src/lib/format/autofill.js.map +1 -0
  20. package/src/lib/format/autofill.mjs +48 -0
  21. package/src/lib/format/autofill.mjs.map +1 -0
  22. package/src/lib/format/bulk-edit.d.mts +18 -0
  23. package/src/lib/format/bulk-edit.d.ts +18 -0
  24. package/src/lib/format/bulk-edit.js +26 -0
  25. package/src/lib/format/bulk-edit.js.map +1 -0
  26. package/src/lib/format/bulk-edit.mjs +21 -0
  27. package/src/lib/format/bulk-edit.mjs.map +1 -0
  28. package/src/lib/format/field.d.mts +55 -34
  29. package/src/lib/format/field.d.ts +55 -34
  30. package/src/lib/format/field.js +16 -2
  31. package/src/lib/format/field.js.map +1 -1
  32. package/src/lib/format/field.mjs +15 -1
  33. package/src/lib/format/field.mjs.map +1 -1
  34. package/src/lib/format/layout.d.mts +365 -66
  35. package/src/lib/format/layout.d.ts +365 -66
  36. package/src/lib/format/layout.js +22 -4
  37. package/src/lib/format/layout.js.map +1 -1
  38. package/src/lib/format/layout.mjs +20 -3
  39. package/src/lib/format/layout.mjs.map +1 -1
  40. package/src/lib/format/page.d.mts +169 -29
  41. package/src/lib/format/page.d.ts +169 -29
  42. package/src/lib/format/page.js +11 -0
  43. package/src/lib/format/page.js.map +1 -1
  44. package/src/lib/format/page.mjs +12 -1
  45. package/src/lib/format/page.mjs.map +1 -1
  46. package/src/lib/format/phone.d.mts +27 -0
  47. package/src/lib/format/phone.d.ts +27 -0
  48. package/src/lib/format/phone.js +43 -0
  49. package/src/lib/format/phone.js.map +1 -0
  50. package/src/lib/format/phone.mjs +39 -0
  51. package/src/lib/format/phone.mjs.map +1 -0
  52. package/src/lib/format/references.d.mts +10 -0
  53. package/src/lib/format/references.d.ts +10 -0
  54. package/src/lib/format/references.js +96 -1
  55. package/src/lib/format/references.js.map +1 -1
  56. package/src/lib/format/references.mjs +96 -1
  57. package/src/lib/format/references.mjs.map +1 -1
  58. package/src/lib/format/slots-options.d.mts +26 -0
  59. package/src/lib/format/slots-options.d.ts +26 -0
  60. package/src/lib/format/slots-options.js +35 -0
  61. package/src/lib/format/slots-options.js.map +1 -0
  62. package/src/lib/format/slots-options.mjs +31 -0
  63. package/src/lib/format/slots-options.mjs.map +1 -0
  64. package/src/lib/format/upgrade.d.mts +4 -1
  65. package/src/lib/format/upgrade.d.ts +4 -1
  66. package/src/lib/format/upgrade.js +4 -1
  67. package/src/lib/format/upgrade.js.map +1 -1
  68. package/src/lib/format/upgrade.mjs +4 -1
  69. package/src/lib/format/upgrade.mjs.map +1 -1
  70. package/src/lib/format/version.d.mts +2 -2
  71. package/src/lib/format/version.d.ts +2 -2
  72. package/src/lib/format/version.js +2 -2
  73. package/src/lib/format/version.js.map +1 -1
  74. package/src/lib/format/version.mjs +2 -2
  75. package/src/lib/format/version.mjs.map +1 -1
  76. package/src/lib/format/zoned.d.mts +8 -0
  77. package/src/lib/format/zoned.d.ts +8 -0
  78. package/src/lib/format/zoned.js +36 -0
  79. package/src/lib/format/zoned.js.map +1 -0
  80. package/src/lib/format/zoned.mjs +33 -0
  81. package/src/lib/format/zoned.mjs.map +1 -0
  82. package/src/lib/record/check.d.mts +6 -1
  83. package/src/lib/record/check.d.ts +6 -1
  84. package/src/lib/record/check.js +12 -4
  85. package/src/lib/record/check.js.map +1 -1
  86. package/src/lib/record/check.mjs +12 -4
  87. package/src/lib/record/check.mjs.map +1 -1
  88. package/src/lib/record/data-source.d.mts +188 -3
  89. package/src/lib/record/data-source.d.ts +188 -3
  90. package/src/lib/record/data-source.js.map +1 -1
  91. package/src/lib/record/data-source.mjs.map +1 -1
  92. package/src/lib/record/form.d.mts +72 -25
  93. package/src/lib/record/form.d.ts +72 -25
  94. package/src/lib/record/form.js +404 -78
  95. package/src/lib/record/form.js.map +1 -1
  96. package/src/lib/record/form.mjs +404 -78
  97. package/src/lib/record/form.mjs.map +1 -1
  98. package/src/lib/record/input-rules.d.mts +51 -9
  99. package/src/lib/record/input-rules.d.ts +51 -9
  100. package/src/lib/record/input-rules.js +119 -20
  101. package/src/lib/record/input-rules.js.map +1 -1
  102. package/src/lib/record/input-rules.mjs +118 -19
  103. package/src/lib/record/input-rules.mjs.map +1 -1
  104. package/src/lib/record/inputs.js +4 -2
  105. package/src/lib/record/inputs.js.map +1 -1
  106. package/src/lib/record/inputs.mjs +4 -2
  107. package/src/lib/record/inputs.mjs.map +1 -1
  108. package/src/lib/record/limits.d.mts +1 -1
  109. package/src/lib/record/limits.d.ts +1 -1
  110. package/src/lib/record/limits.js +4 -2
  111. package/src/lib/record/limits.js.map +1 -1
  112. package/src/lib/record/limits.mjs +4 -2
  113. package/src/lib/record/limits.mjs.map +1 -1
  114. package/src/lib/record/locales/ar.js +3 -2
  115. package/src/lib/record/locales/ar.js.map +1 -1
  116. package/src/lib/record/locales/ar.mjs +3 -2
  117. package/src/lib/record/locales/ar.mjs.map +1 -1
  118. package/src/lib/record/locales/de.js +3 -2
  119. package/src/lib/record/locales/de.js.map +1 -1
  120. package/src/lib/record/locales/de.mjs +3 -2
  121. package/src/lib/record/locales/de.mjs.map +1 -1
  122. package/src/lib/record/locales/fr.js +3 -2
  123. package/src/lib/record/locales/fr.js.map +1 -1
  124. package/src/lib/record/locales/fr.mjs +3 -2
  125. package/src/lib/record/locales/fr.mjs.map +1 -1
  126. package/src/lib/record/memory-data-source.d.mts +13 -2
  127. package/src/lib/record/memory-data-source.d.ts +13 -2
  128. package/src/lib/record/memory-data-source.js +92 -35
  129. package/src/lib/record/memory-data-source.js.map +1 -1
  130. package/src/lib/record/memory-data-source.mjs +93 -36
  131. package/src/lib/record/memory-data-source.mjs.map +1 -1
  132. package/src/lib/record/memory-schedules.d.mts +45 -0
  133. package/src/lib/record/memory-schedules.d.ts +45 -0
  134. package/src/lib/record/memory-schedules.js +81 -0
  135. package/src/lib/record/memory-schedules.js.map +1 -0
  136. package/src/lib/record/memory-schedules.mjs +78 -0
  137. package/src/lib/record/memory-schedules.mjs.map +1 -0
  138. package/src/lib/record/messages.d.mts +3 -1
  139. package/src/lib/record/messages.d.ts +3 -1
  140. package/src/lib/record/messages.js +3 -2
  141. package/src/lib/record/messages.js.map +1 -1
  142. package/src/lib/record/messages.mjs +3 -2
  143. package/src/lib/record/messages.mjs.map +1 -1
  144. package/src/lib/record/periods.d.mts +47 -0
  145. package/src/lib/record/periods.d.ts +47 -0
  146. package/src/lib/record/periods.js +86 -0
  147. package/src/lib/record/periods.js.map +1 -0
  148. package/src/lib/record/periods.mjs +79 -0
  149. package/src/lib/record/periods.mjs.map +1 -0
  150. package/src/lib/record/rules.js +5 -6
  151. package/src/lib/record/rules.js.map +1 -1
  152. package/src/lib/record/rules.mjs +5 -6
  153. package/src/lib/record/rules.mjs.map +1 -1
  154. package/src/lib/record/run.d.mts +44 -8
  155. package/src/lib/record/run.d.ts +44 -8
  156. package/src/lib/record/run.js +155 -26
  157. package/src/lib/record/run.js.map +1 -1
  158. package/src/lib/record/run.mjs +156 -27
  159. package/src/lib/record/run.mjs.map +1 -1
  160. package/src/lib/record/search.d.mts +13 -3
  161. package/src/lib/record/search.d.ts +13 -3
  162. package/src/lib/record/search.js +11 -4
  163. package/src/lib/record/search.js.map +1 -1
  164. package/src/lib/record/search.mjs +11 -4
  165. package/src/lib/record/search.mjs.map +1 -1
  166. package/src/lib/record/zones.d.mts +57 -0
  167. package/src/lib/record/zones.d.ts +57 -0
  168. package/src/lib/record/zones.js +147 -0
  169. package/src/lib/record/zones.js.map +1 -0
  170. package/src/lib/record/zones.mjs +132 -0
  171. package/src/lib/record/zones.mjs.map +1 -0
  172. package/src/runtime.d.mts +12 -5
  173. package/src/runtime.d.ts +12 -5
  174. package/src/runtime.js +32 -2
  175. package/src/runtime.js.map +1 -1
  176. package/src/runtime.mjs +7 -1
  177. package/src/runtime.mjs.map +1 -1
@@ -5,8 +5,9 @@ exports.workAtRisk = workAtRisk;
5
5
  exports.createForm = createForm;
6
6
  exports.notANumber = notANumber;
7
7
  const upgrade_1 = require("../format/upgrade");
8
+ const zoned_1 = require("../format/zoned");
8
9
  const modifier_1 = require("../expression/modifier");
9
- const functions_1 = require("../expression/functions");
10
+ const zones_1 = require("./zones");
10
11
  const check_1 = require("./check");
11
12
  const inputs_1 = require("./inputs");
12
13
  const compute_1 = require("./compute");
@@ -34,15 +35,23 @@ function unsavedWork(state) {
34
35
  }
35
36
  /**
36
37
  * Whether a way out would lose the person's work without their knowing: a
37
- * change of theirs the last save did not send (`unsent`), files still on
38
- * their way, changes Reload set aside — and, `how.changes`, any change not
39
- * saved. What `form.leave()` — the one check every way out goes through —
40
- * asks about, once the save on its way and the files on theirs settled.
41
- * Before a record's first save, a change not saved is not at risk: as in
42
- * 0.20, each way out decides.
38
+ * change of theirs the last save did not send, or text they left since that
39
+ * does not read ("12a" counts as "12" does: `unsent`), files still on
40
+ * their way, changes Reload set aside, the changes of a save refused for a
41
+ * conflict, waiting for Reload or Keep mine (only a source that keeps
42
+ * versions has one) — and, `how.changes`, any change not saved. What
43
+ * `form.leave()` — the one check every way out goes through — asks about,
44
+ * once the save on its way and the files on theirs settled. Before a
45
+ * record's first save, a change not saved is not at risk: as in 0.20, each
46
+ * way out decides.
43
47
  */
44
48
  function workAtRisk(state, how = {}) {
45
- return (state.unsent ?? []).length > 0 || state.setAside != null || (state.uploads ?? []).some(uploads_1.uploadGoing) || (how.changes === true && (state.dirty.length > 0 || Object.keys(state.unread ?? {}).length > 0));
49
+ return ((state.unsent ?? []).length > 0 ||
50
+ state.setAside != null ||
51
+ (state.uploads ?? []).some(uploads_1.uploadGoing) ||
52
+ // The person asked for these to be saved, and someone else's save came first: still theirs, and not saved.
53
+ (state.conflict != null && state.dirty.length > 0) ||
54
+ (how.changes === true && (state.dirty.length > 0 || Object.keys(state.unread ?? {}).length > 0)));
46
55
  }
47
56
  /**
48
57
  * The record state behind a page: values, what changed, what is visible and
@@ -54,6 +63,8 @@ function createForm(options) {
54
63
  const page = (0, upgrade_1.upgradePage)(options.page);
55
64
  return innerForm(page === options.page ? options : { ...options, page }, []).form;
56
65
  }
66
+ /** Whether a run works a person's change out: a page's `change` steps, set off by it, or a run they set off. */
67
+ const worksOut = (chain) => chain.some((key) => key.startsWith('on.change.'));
57
68
  /** A line's cell whose line is no longer there. */
58
69
  const gone = Symbol('gone');
59
70
  const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -66,7 +77,8 @@ const asValue = (values) => values;
66
77
  * its ways out go through that one's check.
67
78
  */
68
79
  function innerForm(options, within, place, outer) {
69
- const { page } = options;
80
+ // Its time zone written into each date and time field naming none, for whatever reads one from its field alone.
81
+ const page = (0, zoned_1.zonedPage)(options.page);
70
82
  const scheduler = options.scheduler ?? scheduler_1.hostScheduler;
71
83
  const messages = options.messages ?? messages_1.MESSAGES.en;
72
84
  const index = indexLayout(page.layout);
@@ -130,7 +142,19 @@ function innerForm(options, within, place, outer) {
130
142
  const warns = [...nodeRules.values()].some((rules) => rules.some((compiled) => compiled.rule.level === 'warning'));
131
143
  const steps = page.layout.type === 'wizard' ? page.layout.children.map((step) => step.id) : [];
132
144
  const draftKey = () => `fieldia:draft:${page.id}:${state.recordId ?? 'new'}`;
133
- const today = () => (0, functions_1.localDay)(options.now?.() ?? new Date());
145
+ /** Today, on the clock of the page's time zone (the browser's own when it names none). */
146
+ const today = () => (0, zones_1.dayIn)(options.now?.() ?? new Date(), page.timeZone);
147
+ const clock = () => (options.now?.() ?? new Date()).getTime();
148
+ /** What a page's bot check saw — its hidden box filled, the captcha's token — and when the page was shown or started again. */
149
+ let bot = { honeypot: false, captcha: null, since: clock() };
150
+ /** What the bot check saw as the answers go; none without one. */
151
+ function botReport() {
152
+ const check = page.botCheck;
153
+ if (!check)
154
+ return undefined;
155
+ const taken = Math.max(0, clock() - bot.since) / 1000;
156
+ return { honeypot: check.honeypot !== false && bot.honeypot, tooFast: taken < (check.minSeconds ?? 3), seconds: Math.round(taken * 10) / 10, ...(check.captcha ? { captcha: bot.captcha } : {}) };
157
+ }
134
158
  // The record's id and the person, as worked-out values read them: the id is the one asked for until the form has its state.
135
159
  let started = false;
136
160
  let editing = options.editing !== false;
@@ -292,16 +316,41 @@ function innerForm(options, within, place, outer) {
292
316
  let generation = 0;
293
317
  /** Closed (`dispose`): a run still going stops quietly, as its dialog or page is gone — no other record replaced it. */
294
318
  let disposed = false;
295
- /** The fields whose last write was the person's: a step's, a rule's or the app's write takes one over. */
319
+ /** Counted at each reset (the person's Discard): a save on its way then lands as the record it stored. */
320
+ let resets = 0;
321
+ /**
322
+ * THE test every late answer passes (`recordShown`): taken as it is asked for, true while the record it was asked
323
+ * for is still the one shown, as it was. A save's answer, a call's, an onchange's, a name looked up, a line's
324
+ * dialog, a record step's — none is laid on another record, nor over the values the person's Discard let go.
325
+ */
326
+ function shownNow() {
327
+ const at = generation;
328
+ const reset = resets;
329
+ // A saved form placed in a record is the record's: another shown round it, its answers are laid nowhere either.
330
+ const round = outer?.recordShown();
331
+ return () => !disposed && at === generation && reset === resets && (!round || round());
332
+ }
333
+ /**
334
+ * The person's claims on the record: the fields whose last write was theirs. A step's, a rule's or the app's
335
+ * write of the whole field takes one over; a write that works the person's change out (the source's onchange, the
336
+ * page's `change` steps it sets off) keeps it.
337
+ */
296
338
  const personal = new Set();
339
+ /**
340
+ * Of a table the person did not write whole: the keys of the lines they changed, added or took away. A line a
341
+ * step adds is none of theirs, and leaves theirs as they are.
342
+ */
343
+ const personalLines = new Map();
297
344
  /** Fields where a file the person picked landed from the store: at risk until a save sends it, a first save or not. */
298
345
  const filed = new Set();
299
346
  /** The record as the last finished save left it — what it sent and what came back; a page of answers: what it sent. Null before one. */
300
347
  let sentAs = null;
301
- /** Counted at each reset (the person's Discard): a save on its way then lands as the record it stored. */
302
- let resets = 0;
303
- /** Above 0 while a delete the person chose goes on: a way out it takes lets their work go, unasked. */
304
- let deletes = 0;
348
+ /**
349
+ * The records being deleted, by the person's choice, each by its generation: a way out of one of them lets their
350
+ * work in it go, unasked. Another record shown meanwhile is asked about as any.
351
+ */
352
+ const deleting = new Map();
353
+ const deletingShown = () => (deleting.get(generation) ?? 0) > 0;
305
354
  /** Each field's last comparison with what was sent, so a value that did not move is not compared again. */
306
355
  const compared = new Map();
307
356
  /** Fields showing a file that did not reach the store: shown until it is sent again or taken away, whatever else changes. */
@@ -439,8 +488,9 @@ function innerForm(options, within, place, outer) {
439
488
  const was = state.values;
440
489
  const shown = state.errors;
441
490
  state = { ...state, ...patch };
442
- // The person's changes the last save did not send, worked out again from the values as they are now.
443
- if (patch.unsent === undefined && patch.values !== undefined) {
491
+ // The person's changes the last save did not send, worked out again from the values as they are now, and from the
492
+ // text they left that does not read.
493
+ if (patch.unsent === undefined && (patch.values !== undefined || patch.unread !== undefined)) {
444
494
  const unsent = unsentNow();
445
495
  if (!sameList(unsent, state.unsent))
446
496
  state = { ...state, unsent };
@@ -469,42 +519,66 @@ function innerForm(options, within, place, outer) {
469
519
  for (const listener of [...heard])
470
520
  listener(payload);
471
521
  }
472
- /** A field written: told to the app, and — a person's change — the page's `change` steps for it run. */
473
- function changed(field, by) {
474
- noteOrigin(field, by);
522
+ /**
523
+ * A field written: told to the app, and — a person's change — the page's `change` steps for it run. `claim` says
524
+ * what of the person's it touches: `line`, the key of the one line of a table the person changed; `keep`, a write
525
+ * that leaves the person's claims as they are (a line a step adds, a write working their change out).
526
+ */
527
+ function changed(field, by, claim = {}) {
528
+ noteOrigin(field, by, claim);
475
529
  emit('change', { field, value: state.values[field], values: state.values, by });
476
530
  const list = page.on?.change?.[field];
477
531
  if (by === 'person' && list)
478
532
  void track(moment(`on.change.${field}`, list, [], true));
479
533
  }
480
- /** Another record is shown: nothing of the last one's is the person's work any more. */
534
+ /** Another record is shown: nothing of the last one's is the person's work any more, and its files on their way stop. */
481
535
  function newGeneration() {
482
536
  generation++;
483
537
  personal.clear();
538
+ personalLines.clear();
484
539
  filed.clear();
485
540
  compared.clear();
486
541
  sentAs = null;
542
+ clearFiles();
487
543
  }
488
544
  /** Whether a field holds a file the store holds that no save sent: its own, or one in a saved form placed here under its name. */
489
545
  const filedIn = (name) => filed.has(name) || [...attached.values()].some((part) => part.name === name && part.inner.files.filed());
490
546
  /**
491
547
  * The person's work no save took, by field, in the page's order: each of
492
548
  * their changes that differs from the record as the last save left it —
493
- * before a first save, only a file the store holds.
549
+ * a field they wrote, or a line of a table they changed — and text they
550
+ * left in a box of it that does not read (`unread`), which no save can
551
+ * send; before a first save, only a file the store holds.
494
552
  */
495
553
  function unsentNow() {
496
- if (!personal.size)
554
+ const typed = sentAs ? unreadFields() : null;
555
+ if (!personal.size && !personalLines.size && !typed?.size)
497
556
  return [];
498
557
  const out = [];
499
558
  for (const name of Object.keys(fields)) {
500
- if (!personal.has(name))
559
+ // Typed and left unread since a save: the person's, as a change is — a value changed to "12" is, so is "12a".
560
+ if (typed?.has(name)) {
561
+ out.push(name);
562
+ continue;
563
+ }
564
+ const lines = personalLines.get(name);
565
+ if (!personal.has(name) && !lines)
501
566
  continue;
502
567
  const from = sentAs ?? (filedIn(name) ? baseline : null);
503
- if (from && differs(name, state.values[name], from[name]))
568
+ if (!from)
569
+ continue;
570
+ if (lines ? linesDiffer(name, lines, state.values[name], from[name]) : differs(name, state.values[name], from[name]))
504
571
  out.push(name);
505
572
  }
506
573
  return out;
507
574
  }
575
+ /** The fields with text left unread in a box of theirs: a field's, a line's cell's, a property's. */
576
+ function unreadFields() {
577
+ const out = new Set();
578
+ for (const path of Object.keys(state.unread ?? {}))
579
+ out.add(path.split('.')[0]);
580
+ return out;
581
+ }
508
582
  function differs(name, value, sent) {
509
583
  const known = compared.get(name);
510
584
  if (known && known.value === value && known.sent === sent)
@@ -513,18 +587,53 @@ function innerForm(options, within, place, outer) {
513
587
  compared.set(name, { value, sent, differs: verdict });
514
588
  return verdict;
515
589
  }
590
+ /** Whether any of these lines of a table differs from the table as sent: changed, added, or taken away. */
591
+ function linesDiffer(name, keys, value, sent) {
592
+ const def = fields[name];
593
+ const find = (lines, key) => (lines ?? []).find((line) => line.key === key);
594
+ for (const key of keys) {
595
+ const now = find(value, key);
596
+ const was = find(sent, key);
597
+ if (!now && !was)
598
+ continue;
599
+ if (!now || !was)
600
+ return true;
601
+ if (now.values === was.values)
602
+ continue;
603
+ for (const sub of new Set([...Object.keys(now.values), ...Object.keys(was.values)])) {
604
+ if (!sameValue(def.fields[sub], now.values[sub], was.values[sub]))
605
+ return true;
606
+ }
607
+ }
608
+ return false;
609
+ }
516
610
  /** `unsent` as it is now, told when it changed. */
517
611
  function refreshUnsent() {
518
612
  const unsent = unsentNow();
519
613
  if (!sameList(unsent, state.unsent))
520
614
  set({ unsent });
521
615
  }
522
- /** A field written, and by whom: the person's change is theirs until a step, a rule or the app writes the field in its place. */
523
- function noteOrigin(field, by) {
524
- if (by === 'person')
525
- personal.add(field);
526
- else
616
+ /**
617
+ * A field written, and by whom: the person's change is theirs — the field, or the line of a table they changed —
618
+ * until a step, a rule or the app writes the whole field in its place. A write that leaves their change where it
619
+ * is (`keep`: a line a step adds, a `change` step working out what they typed) leaves it theirs.
620
+ */
621
+ function noteOrigin(field, by, claim) {
622
+ if (by === 'person') {
623
+ if (claim.line === undefined) {
624
+ personal.add(field);
625
+ personalLines.delete(field);
626
+ }
627
+ else if (!personal.has(field)) {
628
+ const keys = personalLines.get(field) ?? new Set();
629
+ keys.add(claim.line);
630
+ personalLines.set(field, keys);
631
+ }
632
+ }
633
+ else if (!claim.keep) {
527
634
  personal.delete(field);
635
+ personalLines.delete(field);
636
+ }
528
637
  refreshUnsent();
529
638
  }
530
639
  /** A wizard step entered: told to the app, and its `show` steps run. */
@@ -694,10 +803,12 @@ function innerForm(options, within, place, outer) {
694
803
  const ticks = node.source.widget === 'tick' && page.fields[name].type === 'boolean';
695
804
  return ticks && state.values[name] !== true && nodeState(node.id).required ? messages.tick : undefined;
696
805
  }
806
+ /** The widget a field is shown with where it keeps its value its own way (a per cent): the first of its nodes'. */
807
+ const shownWidget = (shown) => shown.map((node) => node.source.widget).find((widget) => widget === 'percentage');
697
808
  /** A field node's error now: its field's own rules first, then its answer rules. */
698
809
  function nodeError(node) {
699
810
  const name = node.field;
700
- return unticked(node) ?? (0, inputs_1.checkInput)(page.fields[name], node.source, state.values[name], messages) ?? (0, check_1.checkValue)(checked(name), state.values[name], nodeState(node.id).required, messages, today()) ?? ruleCheck(node).error;
811
+ return unticked(node) ?? (0, inputs_1.checkInput)(page.fields[name], node.source, state.values[name], messages) ?? (0, check_1.checkValue)(checked(name), state.values[name], nodeState(node.id).required, messages, today(), node.source.widget) ?? ruleCheck(node).error;
701
812
  }
702
813
  /** Warnings for the visible fields of the page, without publishing them. */
703
814
  function collectWarnings() {
@@ -781,7 +892,7 @@ function innerForm(options, within, place, outer) {
781
892
  const cell = ruled[sub];
782
893
  if (cell?.invisible || (tables.has(node.id) && tables.get(node.id).compiled.hidden(sub, reading().context, reading().env, userRoles)))
783
894
  continue;
784
- const lineMessage = (0, check_1.checkValue)(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today());
895
+ const lineMessage = (0, check_1.checkValue)(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today(), node.source.cells?.[sub]?.widget);
785
896
  if (lineMessage)
786
897
  errors[`${name}.${line.key}.${sub}`] = lineMessage;
787
898
  }
@@ -846,7 +957,7 @@ function innerForm(options, within, place, outer) {
846
957
  if ((0, values_1.isEmpty)(fieldDef(field), value))
847
958
  return undefined;
848
959
  return (shown.map((node) => (0, inputs_1.checkInput)(page.fields[field], node.source, value, messages)).find(Boolean) ??
849
- besidesRows(field, (0, check_1.checkValue)(checked(field), value, false, messages, today())) ??
960
+ besidesRows(field, (0, check_1.checkValue)(checked(field), value, false, messages, today(), shownWidget(shown))) ??
850
961
  shown.map((node) => ruleCheck(node).error).find(Boolean));
851
962
  }
852
963
  if (parts.length === 2 && page.fields[field]?.type === 'properties')
@@ -1032,8 +1143,10 @@ function innerForm(options, within, place, outer) {
1032
1143
  return;
1033
1144
  const seq = ++onchangeSeq;
1034
1145
  const request = { model: page.data.model, id: state.recordId, changed, values: (0, values_1.structuredCopy)(state.values) };
1146
+ const shown = shownNow();
1035
1147
  void track(source.onchange(request).then((result) => {
1036
- if (seq !== onchangeSeq)
1148
+ // A newer change asked since; or the record it was asked for is gone, or its values let go: none of it is laid.
1149
+ if (seq !== onchangeSeq || !shown())
1037
1150
  return;
1038
1151
  const values = { ...state.values, ...(result.values ?? {}) };
1039
1152
  writeValues(values, { warning: result.warning ?? null, warningField: result.warning ? changed : null });
@@ -1134,18 +1247,27 @@ function innerForm(options, within, place, outer) {
1134
1247
  set({ status: 'error', error: problem.message, saveProblem: problem, conflict: null });
1135
1248
  return { reason: 'save', message: problem.message };
1136
1249
  }
1250
+ // Asked for the record shown now: another shown while it waits below, it saves nothing of that one, nor says anything there.
1251
+ const asked = generation;
1252
+ const elsewhere = () => ({ reason: 'save', message: messages.recordReplaced });
1137
1253
  // What is still typed in a box is read first: it is what is saved, or why nothing is. A name still being looked
1138
1254
  // up is waited for, so the record it names is what is saved — or, named by none, why nothing is.
1139
1255
  const waiting = readTyped();
1140
1256
  if (waiting)
1141
1257
  await waiting;
1258
+ if (asked !== generation)
1259
+ return elsewhere();
1142
1260
  // Files still on their way are waited for first — a saved form's placed here too: the state says how many, so the page can say why it waits.
1143
1261
  if (filesGoing()) {
1144
1262
  set({ status: 'saving', error: null, saveProblem: null });
1145
1263
  await filesSettled();
1264
+ if (asked !== generation)
1265
+ return elsewhere();
1146
1266
  set({ status: 'ready' });
1147
1267
  }
1148
1268
  if (page.on?.beforeSave) {
1269
+ // Its run stops of itself on a record shown by anything but its own steps (a `call`'s `{ record }`, a reload):
1270
+ // those move the save on with it.
1149
1271
  const before = await moment('on.beforeSave', page.on.beforeSave, chain, false);
1150
1272
  if (!before.done) {
1151
1273
  // A check's problems, or an inner save's, are shown already; any other stop is this save's problem.
@@ -1170,6 +1292,8 @@ function innerForm(options, within, place, outer) {
1170
1292
  try {
1171
1293
  // Bytes still in the record — a signature drawn, a value the app set, in a saved form placed here too — go to the store first, where it keeps files.
1172
1294
  await storeBytes(page.data.kind === 'responses' || state.recordId == null ? Object.keys(fields) : state.dirty);
1295
+ if (at !== generation)
1296
+ return elsewhere();
1173
1297
  const failed = fileProblems();
1174
1298
  if (Object.keys(failed).length) {
1175
1299
  set({ errors: failed, ...unstored(failed) });
@@ -1181,7 +1305,11 @@ function innerForm(options, within, place, outer) {
1181
1305
  const sent = shownValues();
1182
1306
  const before = state.values;
1183
1307
  at = generation;
1184
- await source.submit({ pageId: page.id, values: sent });
1308
+ const seen = botReport();
1309
+ await source.submit({ pageId: page.id, values: sent, ...(seen ? { bot: seen } : {}) });
1310
+ // The captcha's token is spent once sent: the next send waits for a new one.
1311
+ if (seen?.captcha)
1312
+ bot.captcha = null;
1185
1313
  // Answered for answers no longer shown — the form was started again meanwhile: the source has them, the form lays nothing.
1186
1314
  if (at !== generation)
1187
1315
  return null;
@@ -1189,7 +1317,7 @@ function innerForm(options, within, place, outer) {
1189
1317
  // What it sent is what an answer changed since is told against: still to send.
1190
1318
  sentAs = before;
1191
1319
  set({ status: 'saved', unsent: unsentNow() });
1192
- emit('send', { values: sent });
1320
+ emit('send', { values: sent, ...(seen ? { bot: seen } : {}) });
1193
1321
  void track(moment('on.afterSave', page.on?.afterSave, chain, false));
1194
1322
  return null;
1195
1323
  }
@@ -1225,7 +1353,8 @@ function innerForm(options, within, place, outer) {
1225
1353
  scheduleDraft();
1226
1354
  scheduleAutosave();
1227
1355
  }
1228
- emit('save', { recordId: state.recordId, values: state.values });
1356
+ // The record as this save left it: a change made while it was on its way is no value of it.
1357
+ emit('save', { recordId: state.recordId, values: baseline });
1229
1358
  void track(moment('on.afterSave', page.on?.afterSave, chain, false));
1230
1359
  return null;
1231
1360
  }
@@ -1281,8 +1410,12 @@ function innerForm(options, within, place, outer) {
1281
1410
  }
1282
1411
  /** The form has its values: told to the app, its `open` steps run, then the wizard step it shows is told. */
1283
1412
  async function opening() {
1413
+ const at = generation;
1284
1414
  emit('open', { recordId: state.recordId, values: state.values });
1285
1415
  await moment('on.open', page.on?.open, [], true);
1416
+ // Another record shown while its open steps ran: that one's opening tells its own steps.
1417
+ if (at !== generation)
1418
+ return;
1286
1419
  showing = true;
1287
1420
  tellStep([]);
1288
1421
  }
@@ -1303,8 +1436,8 @@ function innerForm(options, within, place, outer) {
1303
1436
  for (const name of names)
1304
1437
  changed(name, 'app');
1305
1438
  }
1306
- /** Write fields — a step's or the app's — worked out once, and tell each. */
1307
- function write(written, by) {
1439
+ /** Write fields — a step's or the app's — worked out once, and tell each; `keep`, a write working the person's change out. */
1440
+ function write(written, by, keep = false) {
1308
1441
  const names = Object.keys(written);
1309
1442
  names.forEach(fieldDef);
1310
1443
  if (!names.length)
@@ -1313,8 +1446,9 @@ function innerForm(options, within, place, outer) {
1313
1446
  for (const name of names)
1314
1447
  afterEdit(name);
1315
1448
  for (const name of names)
1316
- changed(name, by);
1449
+ changed(name, by, { keep });
1317
1450
  }
1451
+ /** A line added: the person's, theirs; a step's, none of theirs, and the lines they changed stay theirs. */
1318
1452
  function addLine(field, values, by) {
1319
1453
  const def = lineField(field);
1320
1454
  const current = state.values[field] ?? [];
@@ -1337,7 +1471,7 @@ function innerForm(options, within, place, outer) {
1337
1471
  const lines = [...current, line];
1338
1472
  writeValues({ ...state.values, [field]: lines });
1339
1473
  afterEdit(field);
1340
- changed(field, by);
1474
+ changed(field, by, by === 'person' ? { line: key } : { keep: true });
1341
1475
  return key;
1342
1476
  }
1343
1477
  /**
@@ -1346,15 +1480,44 @@ function innerForm(options, within, place, outer) {
1346
1480
  */
1347
1481
  async function check(names) {
1348
1482
  names?.forEach(fieldDef);
1483
+ const at = generation;
1349
1484
  const waiting = readTyped();
1350
1485
  if (waiting)
1351
1486
  await waiting;
1487
+ // Another record shown while it waited: nothing found is shown on that one.
1488
+ if (at !== generation)
1489
+ return replaced();
1352
1490
  const all = collectErrors();
1353
1491
  const found = names ? Object.fromEntries(Object.entries(all).filter(([key]) => names.some((name) => key === name || key.startsWith(`${name}.`)))) : all;
1354
1492
  set({ errors: found });
1355
1493
  const first = Object.values(found)[0];
1356
1494
  return first === undefined ? null : { reason: 'check', message: first };
1357
1495
  }
1496
+ /**
1497
+ * What is typed read first, for a run the person or the app set off (`RunScope.typed`): every box read as a save
1498
+ * reads it, a name looked up waited for. Text left that does not read stops the run, said under its box as a check
1499
+ * says it — never a value under it handed on or worked from.
1500
+ */
1501
+ async function typedFirst() {
1502
+ const at = generation;
1503
+ const waiting = readTyped();
1504
+ if (waiting)
1505
+ await waiting;
1506
+ // Another record shown while it waited: the run is not for that one.
1507
+ if (at !== generation)
1508
+ return replaced();
1509
+ const found = unreadProblems();
1510
+ const first = Object.values(found)[0];
1511
+ if (first === undefined)
1512
+ return null;
1513
+ set({ errors: { ...state.errors, ...found } });
1514
+ return { reason: 'check', message: first };
1515
+ }
1516
+ /** The problems of text left unread in boxes shown — this form's and its saved forms' placed here — by path. */
1517
+ function unreadProblems() {
1518
+ const inner = (path) => [...attached.values()].some((part) => path.startsWith(`${part.name}.`) && !!part.inner.form.getState().unread?.[path.slice(part.name.length + 1)]);
1519
+ return Object.fromEntries(Object.entries(collectErrors()).filter(([path]) => !!state.unread[path] || inner(path)));
1520
+ }
1358
1521
  /** Go to a wizard's step for a run; undefined when the target is no step of it, but a tab. */
1359
1522
  async function goToStep(target, chain) {
1360
1523
  if (!steps.includes(target))
@@ -1362,16 +1525,19 @@ function innerForm(options, within, place, outer) {
1362
1525
  if (!form.steps().includes(target))
1363
1526
  return { reason: 'cannot', message: `The step "${target}" does not apply now` };
1364
1527
  // A name still being looked up in a box on the way is waited for, as a save waits for it.
1528
+ const at = generation;
1365
1529
  const waiting = readTyped();
1366
1530
  if (waiting)
1367
1531
  await waiting;
1532
+ if (at !== generation)
1533
+ return replaced();
1368
1534
  let went = false;
1369
1535
  inRun(chain, () => (went = form.goTo(target)));
1370
1536
  return went ? null : { reason: 'check', message: Object.values(state.errors)[0] };
1371
1537
  }
1372
1538
  /** Run steps from the top — a button's, a moment's, the app's — and tell how it ended: on the record shown as it starts (`generation`). */
1373
1539
  async function runTop(list, scope) {
1374
- scope.generation = generation;
1540
+ scope.generation ??= generation;
1375
1541
  const result = await runner.runSteps(list, scope);
1376
1542
  emit('run', { id: scope.id, steps: list, result });
1377
1543
  return result;
@@ -1389,7 +1555,7 @@ function innerForm(options, within, place, outer) {
1389
1555
  throw new Error(`"${name}" cannot make records: its data source has no create, or it is not a link`);
1390
1556
  return track(source.create({ model, name: typed.trim(), ...(Object.keys(values).length ? { values } : {}) }));
1391
1557
  }
1392
- async function runSearch(def, name, ctx, query, limit, model, ids) {
1558
+ async function runSearch(def, name, ctx, query, limit, { model, ids, exact } = {}) {
1393
1559
  // Formatted text names people and records in place (@mention, #4127): searched by the model they are of.
1394
1560
  const byModel = def.type === 'reference' || def.type === 'json' || def.type === 'html';
1395
1561
  if (def.type !== 'many2one' && def.type !== 'many2many' && !byModel) {
@@ -1404,7 +1570,7 @@ function innerForm(options, within, place, outer) {
1404
1570
  const own = byModel || !def.filter ? undefined : (0, filter_1.resolveFilter)(def.filter, ctx);
1405
1571
  const filter = ids ? [...(own ?? []), { field: 'id', op: 'in', value: ids }] : own;
1406
1572
  const relation = byModel ? model : def.relation;
1407
- return track(source.search({ model: relation, query, ...(filter ? { filter } : {}), limit }));
1573
+ return track(source.search({ model: relation, query, ...(filter ? { filter } : {}), limit, ...(exact ? { exact: true } : {}) }));
1408
1574
  }
1409
1575
  function lineField(field) {
1410
1576
  const def = fieldDef(field);
@@ -1505,7 +1671,7 @@ function innerForm(options, within, place, outer) {
1505
1671
  }
1506
1672
  }
1507
1673
  /** Whether a way out goes at once: nothing to wait for, no check going, nothing at risk — as in 0.20. */
1508
- const atOnce = (how, own = false) => !deletes && !checking && !busy(own) && (how.discard === true || !workAtRisk(state, how));
1674
+ const atOnce = (how, own = false) => !deletingShown() && !checking && !busy(own) && (how.discard === true || !workAtRisk(state, how));
1509
1675
  /** The saves on their way, settled: what a load that is not a way out (a conflict's Reload) waits for. */
1510
1676
  async function savesSettled() {
1511
1677
  while (saveRunning)
@@ -1531,10 +1697,13 @@ function innerForm(options, within, place, outer) {
1531
1697
  if (outer)
1532
1698
  return outer.letGo(how, chain);
1533
1699
  // A delete the person chose: their work goes with the record, unasked.
1534
- if (deletes) {
1700
+ if (deletingShown()) {
1535
1701
  forget();
1536
1702
  return true;
1537
1703
  }
1704
+ // The person's own Discard stops the files on their way, never waits for them.
1705
+ if (how.discard)
1706
+ clearFiles();
1538
1707
  const own = chain.includes('on.beforeSave');
1539
1708
  if (atOnce(how, own)) {
1540
1709
  if (how.discard)
@@ -1577,13 +1746,46 @@ function innerForm(options, within, place, outer) {
1577
1746
  forget();
1578
1747
  return yes;
1579
1748
  }
1580
- /** Let go of the person's work at risk: files on their way stop, changes set aside go, changes no save took are asked about no more. */
1749
+ /**
1750
+ * Let go of the person's work at risk: files on their way stop, changes set aside go, a conflict's changes are
1751
+ * asked about no more, and the changes no save took go back to the record as the last save left it — so what a
1752
+ * dialog or a page in place hands back once it saved is the record as saved, never the change let go.
1753
+ */
1581
1754
  function forget() {
1755
+ const back = unsentLetGo();
1582
1756
  clearFiles();
1583
1757
  personal.clear();
1758
+ personalLines.clear();
1584
1759
  filed.clear();
1585
- if (state.setAside !== null || state.unsent.length)
1586
- set({ setAside: null, unsent: [] });
1760
+ if (back)
1761
+ writeValues({ ...state.values, ...back });
1762
+ // Text left that does not read goes too: each box shows what its field holds.
1763
+ const unread = Object.keys(state.unread).length ? { unread: {} } : {};
1764
+ if (state.setAside !== null || state.unsent.length || state.conflict !== null || unread.unread)
1765
+ set({ setAside: null, unsent: [], conflict: null, ...unread });
1766
+ }
1767
+ /** The fields holding the person's changes no save took, as the record as last saved holds them: their lines alone, for a table. */
1768
+ function unsentLetGo() {
1769
+ if (!state.unsent.length)
1770
+ return null;
1771
+ const out = {};
1772
+ for (const name of state.unsent) {
1773
+ const from = sentAs ?? baseline;
1774
+ const keys = personalLines.get(name);
1775
+ out[name] = keys && !personal.has(name) ? linesAsSent(state.values[name], from[name], keys) : (0, values_1.structuredCopy)(from[name]);
1776
+ }
1777
+ return out;
1778
+ }
1779
+ /** A table with these lines as sent: changed ones as they were, added ones gone, ones taken away back in their place. */
1780
+ function linesAsSent(value, sent, keys) {
1781
+ const was = sent ?? [];
1782
+ const of = (key) => was.find((line) => line.key === key);
1783
+ const out = (value ?? []).filter((line) => !keys.has(line.key) || of(line.key)).map((line) => (keys.has(line.key) ? (0, values_1.structuredCopy)(of(line.key)) : line));
1784
+ was.forEach((line, at) => {
1785
+ if (keys.has(line.key) && !out.some((other) => other.key === line.key))
1786
+ out.splice(Math.min(at, out.length), 0, (0, values_1.structuredCopy)(line));
1787
+ });
1788
+ return out;
1587
1789
  }
1588
1790
  /** The check as a step meets it: null to go on — at once when nothing is at risk — else why the run stops. */
1589
1791
  function letGoStep(chain) {
@@ -1591,12 +1793,19 @@ function innerForm(options, within, place, outer) {
1591
1793
  return go === true ? null : go.then((yes) => (yes ? null : kept()));
1592
1794
  }
1593
1795
  const kept = () => ({ reason: 'no', message: messages.discardChanges });
1594
- /** Load the record again, once the check let what is shown go. */
1595
- function loadAgain(again, chain = []) {
1796
+ /**
1797
+ * A step's own move — the record loaded again, or another shown — once the check let what is shown go: kept, the
1798
+ * person kept their work; replaced, another record was shown before it ended (the run's steps are not for that one).
1799
+ */
1800
+ async function ownMove(chain, move) {
1596
1801
  const go = letGo({}, chain);
1597
- if (go === true)
1598
- return load(again).then(() => true);
1599
- return go.then((yes) => (yes ? load(again).then(() => true) : false));
1802
+ if (go !== true && !(await go))
1803
+ return kept();
1804
+ // The record it shows is a generation of its own, made as it starts: another made before it ends replaced it.
1805
+ const moving = move();
1806
+ const mine = generation;
1807
+ await moving;
1808
+ return mine === generation ? null : replaced();
1600
1809
  }
1601
1810
  /**
1602
1811
  * The host's close, as every `close` step reaches it: the form takes it
@@ -1618,6 +1827,10 @@ function innerForm(options, within, place, outer) {
1618
1827
  const go = letGo({}, chain);
1619
1828
  return go === true ? shut() : go.then((yes) => (yes ? shut() : kept()));
1620
1829
  }
1830
+ /** Why a run stops whose record was replaced while it went on — quietly, when the form was closed. */
1831
+ function replaced() {
1832
+ return disposed ? { reason: 'closed' } : { reason: 'cannot', message: messages.recordReplaced };
1833
+ }
1621
1834
  /** The record a record step asks the data source about. */
1622
1835
  const record = () => ({ model: page.data.model, id: state.recordId, fields });
1623
1836
  async function post(message) {
@@ -1640,18 +1853,30 @@ function innerForm(options, within, place, outer) {
1640
1853
  recordId: () => state.recordId,
1641
1854
  reading,
1642
1855
  today,
1643
- write: (values) => write(values, 'step'),
1856
+ // A step's write of a field takes the person's change in it over — unless the step works that change out: the
1857
+ // page's `change` steps, set off by it, as the source's onchange does.
1858
+ write: (values, chain) => write(values, 'step', worksOut(chain)),
1644
1859
  store: (values) => store(values),
1860
+ // The person's own change: the field's, or a line of it (a claim by line): put back, the whole field is theirs
1861
+ // again — at risk, asked about — never less than it was.
1862
+ mine: (field) => personal.has(field) || personalLines.has(field),
1863
+ putBack(values, mine) {
1864
+ write(values, 'step');
1865
+ for (const name of mine)
1866
+ noteOrigin(name, 'person', {});
1867
+ },
1645
1868
  addLine: (field, values) => void addLine(field, values, 'step'),
1646
1869
  check,
1870
+ typedFirst,
1647
1871
  save: saving,
1648
1872
  generation: () => generation,
1649
- gone: () => (disposed ? { reason: 'closed' } : { reason: 'cannot', message: messages.recordReplaced }),
1873
+ gone: replaced,
1650
1874
  async reload(chain) {
1651
1875
  if (state.recordId == null)
1652
1876
  return { reason: 'cannot', message: 'The record is not saved yet: there is nothing to load again' };
1653
- if (!(await loadAgain(true, chain)))
1654
- return kept();
1877
+ const stopped = await ownMove(chain, () => load(true));
1878
+ if (stopped)
1879
+ return stopped;
1655
1880
  return state.status === 'error' ? { reason: 'cannot', message: state.error ?? 'The record could not be loaded again' } : null;
1656
1881
  },
1657
1882
  reset(chain) {
@@ -1672,24 +1897,32 @@ function innerForm(options, within, place, outer) {
1672
1897
  changed: () => state.dirty.length > 0 || state.uploads.some(uploads_1.uploadGoing),
1673
1898
  letGo: letGoStep,
1674
1899
  async deleting(remove, done) {
1675
- deletes++;
1900
+ const of = generation;
1901
+ const shown = shownNow();
1902
+ deleting.set(of, (deleting.get(of) ?? 0) + 1);
1676
1903
  try {
1677
1904
  const out = await remove();
1678
- // Deleted: what the person had not saved in it goes with it. Refused, it stays theirs.
1679
- if (done(out))
1905
+ // Deleted: what the person had not saved in it goes with it. Refused, it stays theirs. Another record shown
1906
+ // meanwhile keeps what was typed in it.
1907
+ if (done(out) && shown())
1680
1908
  forget();
1681
1909
  return out;
1682
1910
  }
1683
1911
  finally {
1684
- deletes--;
1912
+ const left = (deleting.get(of) ?? 1) - 1;
1913
+ if (left)
1914
+ deleting.set(of, left);
1915
+ else
1916
+ deleting.delete(of);
1685
1917
  }
1686
1918
  },
1687
1919
  ...(options.dataSource?.archive && page.data.kind === 'record' ? { archive: (archive) => track((options.dataSource?.archive)({ ...record(), archive })) } : {}),
1688
1920
  ...(options.dataSource?.copy && page.data.kind === 'record' ? { copy: async () => (await track((options.dataSource?.copy)(record()))).id } : {}),
1689
1921
  ...(options.dataSource?.delete && page.data.kind === 'record' ? { remove: () => track((options.dataSource?.delete)(record())) } : {}),
1690
1922
  async showRecord(id, chain) {
1691
- if (!(await openRecord(id, chain)))
1692
- return kept();
1923
+ const stopped = await ownMove(chain, () => showNow(id));
1924
+ if (stopped)
1925
+ return stopped;
1693
1926
  return state.status === 'error' ? { reason: 'cannot', message: state.error ?? 'The record could not be loaded' } : null;
1694
1927
  },
1695
1928
  told(event, id) {
@@ -1708,8 +1941,24 @@ function innerForm(options, within, place, outer) {
1708
1941
  wordsOf: (field) => wordsOfValue(fieldDef(field), state.values[field]),
1709
1942
  barred: (right) => (state.can?.[right] === false ? state.can.reason ?? messages[MAY_NOT[right]] : null),
1710
1943
  });
1711
- const moment = (0, run_1.createMoments)((key, list, chain) => runTop(list, { id: key, chain }));
1712
- /** A press of a button: its confirmation first, then its steps and its app action as a final call, in `scope`. */
1944
+ // A moment's run is on the record it was set off on: one set off again while its run goes, and run after it, is let
1945
+ // go when another record is shown by then (its steps were for that one).
1946
+ const moment = (0, run_1.createMoments)((key, list, chain, on) => runTop(list, { id: key, chain, generation: on }), () => generation);
1947
+ /**
1948
+ * Whether steps do something before their first save — set a status, call the app, open a page — so a record's
1949
+ * button made of them saves first. Steps that start with the save (Save and close), or only check before it, need
1950
+ * no save before them: theirs is the first.
1951
+ */
1952
+ function actsBeforeSaving(list) {
1953
+ const first = list.findIndex((step) => step.do === 'save');
1954
+ return first > 0 && list.slice(0, first).some((step) => step.do !== 'check');
1955
+ }
1956
+ /**
1957
+ * A press of a button: its confirmation first, then its steps and its app action as a final call, in `scope`. A
1958
+ * record's button whose steps act and then save saves the record first when it is new or changed, as Flectra's
1959
+ * button does: refused — a field it requires left empty — none of its steps run, so nothing it does happens to a
1960
+ * record that cannot be saved.
1961
+ */
1713
1962
  function press(source, scope) {
1714
1963
  const list = [...(source.steps ?? [])];
1715
1964
  if (source.action !== undefined)
@@ -1720,12 +1969,37 @@ function innerForm(options, within, place, outer) {
1720
1969
  list.push({ do: builtin });
1721
1970
  const asked = builtin === 'archive' ? messages.archiveConfirm : builtin === 'delete' ? messages.deleteConfirm : undefined;
1722
1971
  const confirm = 'confirm' in source && source.confirm !== undefined ? source.confirm : asked;
1972
+ // On the record it was pressed on: another shown while its question is asked, it does nothing to that one.
1973
+ scope.generation ??= generation;
1974
+ // Pressed by the person: what is typed is read before a step reads the form or hands its values on.
1975
+ scope.typed ??= 'first';
1723
1976
  return track((async () => {
1724
1977
  if (confirm && !(await runner.ask(confirm))) {
1725
1978
  const result = { done: false, reason: 'no', message: confirm };
1726
1979
  emit('run', { id: scope.id, steps: list, result });
1727
1980
  return result;
1728
1981
  }
1982
+ const saveFirst = page.data.kind === 'record' && actsBeforeSaving(list);
1983
+ // What is typed is read first, as the run reads it before its first step that reads the form (run.ts): text
1984
+ // that does not read stops it with `check`, said under its box — before the save that comes first, and any step.
1985
+ if (saveFirst && scope.typed === 'first') {
1986
+ const unread = await typedFirst();
1987
+ if (unread) {
1988
+ const at = Math.max(0, list.findIndex((step) => step.do !== 'check'));
1989
+ const result = { done: false, stoppedAt: at, step: list[at], reason: unread.reason, ...(unread.message !== undefined ? { message: unread.message } : {}) };
1990
+ emit('run', { id: scope.id, steps: list, result });
1991
+ return result;
1992
+ }
1993
+ }
1994
+ // Then the save first, then the steps.
1995
+ if (saveFirst && (state.recordId == null || state.dirty.length > 0 || Object.keys(state.unread ?? {}).length > 0 || state.uploads.some(uploads_1.uploadGoing))) {
1996
+ const first = await saving(scope.chain);
1997
+ if (first) {
1998
+ const result = { done: false, step: { do: 'save' }, reason: first.reason, ...(first.message !== undefined ? { message: first.message } : {}), ...(first.error !== undefined ? { error: first.error } : {}) };
1999
+ emit('run', { id: scope.id, steps: list, result });
2000
+ return result;
2001
+ }
2002
+ }
1729
2003
  return runTop(list, scope);
1730
2004
  })());
1731
2005
  }
@@ -1751,6 +2025,7 @@ function innerForm(options, within, place, outer) {
1751
2025
  openRecord: (id) => track(openRecord(id)),
1752
2026
  leave: (how) => Promise.resolve(letGo(how)),
1753
2027
  leaveNow: (how = {}) => (outer ? outer.leaveNow(how) : atOnce(how)),
2028
+ recordShown: shownNow,
1754
2029
  post,
1755
2030
  setEditing(next) {
1756
2031
  if (next === editing)
@@ -1779,7 +2054,7 @@ function innerForm(options, within, place, outer) {
1779
2054
  }
1780
2055
  const own = shown.map(unticked).find(Boolean) ??
1781
2056
  shown.map((node) => (0, inputs_1.checkInput)(page.fields[name], node.source, state.values[name], messages)).find(Boolean) ??
1782
- (0, check_1.checkValue)(checked(name), state.values[name], shown.some((node) => nodeState(node.id).required), messages, today());
2057
+ (0, check_1.checkValue)(checked(name), state.values[name], shown.some((node) => nodeState(node.id).required), messages, today(), shownWidget(shown));
1783
2058
  return own ?? shown.map((node) => ruleCheck(node).error).find((error) => error !== undefined) ?? null;
1784
2059
  },
1785
2060
  setUnread(path, unread) {
@@ -1825,6 +2100,12 @@ function innerForm(options, within, place, outer) {
1825
2100
  };
1826
2101
  },
1827
2102
  settle: () => readTyped() ?? Promise.resolve(),
2103
+ botCheck(seen) {
2104
+ if (seen.honeypot !== undefined)
2105
+ bot.honeypot = seen.honeypot;
2106
+ if (seen.captcha !== undefined)
2107
+ bot.captcha = seen.captcha;
2108
+ },
1828
2109
  reading: () => readingNow > 0 || [...attached.values()].some((part) => part.inner.form.reading()),
1829
2110
  validate() {
1830
2111
  readTyped();
@@ -1834,6 +2115,27 @@ function innerForm(options, within, place, outer) {
1834
2115
  },
1835
2116
  save: () => track(save()),
1836
2117
  canUpload: () => typeof options.dataSource?.upload === 'function',
2118
+ today,
2119
+ canFindSlots: () => typeof options.dataSource?.slots === 'function',
2120
+ findSlots(field, ask) {
2121
+ const source = options.dataSource;
2122
+ if (!source?.slots)
2123
+ return Promise.reject(new Error('The data source has no slots()'));
2124
+ const def = page.fields[field];
2125
+ const { schedule = field, length, between, from, to } = ask;
2126
+ return source.slots({
2127
+ model: page.data.kind === 'record' ? page.data.model : '',
2128
+ field,
2129
+ recordId: state.recordId,
2130
+ schedule,
2131
+ from,
2132
+ to,
2133
+ timeZone: (def?.type === 'datetime' && def.timeZone) || page.timeZone || (0, zones_1.ownZone)(),
2134
+ ...(length ? { length } : {}),
2135
+ ...(between ? { between } : {}),
2136
+ values: (0, values_1.structuredCopy)(state.values),
2137
+ });
2138
+ },
1837
2139
  upload: (field, files, how = {}) => uploads.start(field, files, how),
1838
2140
  uploading: (field) => state.uploads.filter((upload) => upload.field === field && !upload.line),
1839
2141
  cancelUpload(id) {
@@ -1859,8 +2161,11 @@ function innerForm(options, within, place, outer) {
1859
2161
  // The person's own Discard: nothing of theirs is left at risk, and a save on its way lands as the record it stored.
1860
2162
  resets++;
1861
2163
  personal.clear();
2164
+ personalLines.clear();
1862
2165
  filed.clear();
1863
2166
  sentAs = null;
2167
+ // Started again: a new response, timed from now.
2168
+ bot = { ...bot, honeypot: false, since: clock() };
1864
2169
  const values = syncParts((0, values_1.structuredCopy)(baseline), 'start');
1865
2170
  set({ values, dirty: [], errors: {}, warnings: withValues(values, collectWarnings), warning: null, warningField: null, draft: null, step: steps[0] ?? null, skipped: [], saveProblem: null, conflict: null, unsent: [], unread: {} });
1866
2171
  },
@@ -1873,7 +2178,7 @@ function innerForm(options, within, place, outer) {
1873
2178
  const lines = (state.values[field] ?? []).map((line) => line.key === key ? { ...line, values: { ...line.values, [name]: (0, values_1.structuredCopy)(value) } } : line);
1874
2179
  writeValues({ ...state.values, [field]: lines });
1875
2180
  afterEdit(field);
1876
- changed(field, 'person');
2181
+ changed(field, 'person', { line: key });
1877
2182
  },
1878
2183
  moveLine(field, key, to) {
1879
2184
  const def = lineField(field);
@@ -1918,10 +2223,10 @@ function innerForm(options, within, place, outer) {
1918
2223
  const lines = (state.values[field] ?? []).filter((line) => line.key !== key);
1919
2224
  writeValues({ ...state.values, [field]: lines });
1920
2225
  afterEdit(field);
1921
- changed(field, 'person');
2226
+ changed(field, 'person', { line: key });
1922
2227
  },
1923
2228
  async search(field, query, limit = 8, options = {}) {
1924
- return runSearch(fieldDef(field), field, context(), query, limit, options.model, options.ids);
2229
+ return runSearch(fieldDef(field), field, context(), query, limit, options);
1925
2230
  },
1926
2231
  async previewLine(field, key, values) {
1927
2232
  lineField(field);
@@ -1946,7 +2251,7 @@ function innerForm(options, within, place, outer) {
1946
2251
  const line = (state.values[field] ?? []).find((l) => l.key === key);
1947
2252
  if (!line)
1948
2253
  throw new Error(`"${field}" has no line "${key}"`);
1949
- return runSearch(sub, subfield, (0, env_1.lineContext)(line.values, def.fields, context(), line.id ?? null), query, limit, options.model, options.ids);
2254
+ return runSearch(sub, subfield, (0, env_1.lineContext)(line.values, def.fields, context(), line.id ?? null), query, limit, options);
1950
2255
  },
1951
2256
  canCreate: (field) => creatable(page.fields[field]) !== null,
1952
2257
  quickCreate: (field, typed) => runCreate(page.fields[field], field, typed, page.fields[field] ? createValuesOf(field) : {}),
@@ -2108,7 +2413,7 @@ function innerForm(options, within, place, outer) {
2108
2413
  lines: { field: source.field, keys: [...keys], ids: lines.flatMap((line) => (line.id === undefined ? [] : [line.id])), values: lines.map((line) => (0, values_1.structuredCopy)(line.values)) },
2109
2414
  });
2110
2415
  },
2111
- run: (list, context = {}) => track(runTop(list, { id: context.id ?? 'run', chain: [], ...(context.recordIds ? { recordIds: [...context.recordIds] } : {}) })),
2416
+ run: (list, context = {}) => track(runTop(list, { id: context.id ?? 'run', chain: [], typed: 'first', ...(context.recordIds ? { recordIds: [...context.recordIds] } : {}) })),
2112
2417
  shown(tab) {
2113
2418
  // A wizard's steps are told by the form itself, as they are entered.
2114
2419
  if (steps.includes(tab))
@@ -2135,20 +2440,36 @@ function innerForm(options, within, place, outer) {
2135
2440
  },
2136
2441
  async reloadTheirs() {
2137
2442
  // A save on its way first: its answer is not dropped by the load.
2443
+ const asked = shownNow();
2138
2444
  await savesSettled();
2139
- // The person's changes, kept before theirs are loaded over them.
2445
+ if (!asked())
2446
+ return false;
2447
+ // What is still typed is the person's too: read, a name looked up waited for, before theirs are loaded.
2448
+ const waiting = readTyped();
2449
+ if (waiting)
2450
+ await waiting;
2451
+ if (!asked())
2452
+ return false;
2453
+ // The person's changes, kept before theirs are loaded over them; and text they left that does not read.
2140
2454
  const mine = {};
2141
2455
  for (const name of state.dirty)
2142
2456
  mine[name] = (0, values_1.structuredCopy)(state.values[name]);
2143
- await track(load(true));
2144
- if (state.status !== 'ready')
2457
+ const typed = state.unread;
2458
+ // The record loaded again is a record shown anew: what is set aside is laid on it alone.
2459
+ const loading = load(true);
2460
+ const loaded = generation;
2461
+ await track(loading);
2462
+ if (loaded !== generation || state.status !== 'ready')
2145
2463
  return false;
2146
2464
  // Set aside where theirs differ from mine: a change they made the same way is no loss.
2147
2465
  const aside = { ...(state.setAside ?? {}) };
2148
2466
  for (const [name, value] of Object.entries(mine))
2149
2467
  if (!sameValue(fields[name], value, state.values[name]))
2150
2468
  aside[name] = value;
2151
- set({ conflict: null, saveProblem: null, setAside: Object.keys(aside).length ? aside : null });
2469
+ // Text that does not read is no value to set aside: it stays in its box, unread with why, over their value —
2470
+ // never dropped unsaid. A line of theirs no longer there takes its cells' text with it.
2471
+ const unread = Object.fromEntries(Object.entries(typed).filter(([path]) => valueAt(state.values, path) !== gone));
2472
+ set({ conflict: null, saveProblem: null, setAside: Object.keys(aside).length ? aside : null, ...(Object.keys(unread).length ? { unread } : {}) });
2152
2473
  return true;
2153
2474
  },
2154
2475
  async keepMine() {
@@ -2159,6 +2480,7 @@ function innerForm(options, within, place, outer) {
2159
2480
  const source = options.dataSource;
2160
2481
  if (conflict && !told) {
2161
2482
  if (source?.load && state.recordId != null && page.data.kind === 'record') {
2483
+ const shown = shownNow();
2162
2484
  try {
2163
2485
  const now = (0, data_source_1.loadedRecordOf)(await track(source.load({ model: page.data.model, id: state.recordId, fields })));
2164
2486
  told = now.values;
@@ -2167,6 +2489,9 @@ function innerForm(options, within, place, outer) {
2167
2489
  catch {
2168
2490
  // Unread: said below.
2169
2491
  }
2492
+ // Read, or not, once another record is shown or the values let go: nothing is laid, nothing saved.
2493
+ if (!shown())
2494
+ return false;
2170
2495
  }
2171
2496
  // Still unread: saving now would go over work this person never saw. Nothing is saved; they are told, and choose again.
2172
2497
  if (!told) {
@@ -2229,7 +2554,8 @@ function innerForm(options, within, place, outer) {
2229
2554
  const given = state.values[part.name];
2230
2555
  // A record of no model of its own: its links search and its lists load, but it never loads, saves, sends or recalculates alone.
2231
2556
  const made = innerForm({
2232
- page: { ...inner, data: { kind: 'responses' } },
2557
+ // In the record's time zone, unless it names its own.
2558
+ page: { ...inner, data: { kind: 'responses' }, ...(inner.timeZone || !page.timeZone ? {} : { timeZone: page.timeZone }) },
2233
2559
  dataSource: options.dataSource,
2234
2560
  values: isRecord(given) ? given : {},
2235
2561
  onAction: options.onAction,
@@ -2242,7 +2568,7 @@ function innerForm(options, within, place, outer) {
2242
2568
  // Its files are the record's: stored for it, under the part's name.
2243
2569
  { model: where.model, recordId: where.recordId, under: `${where.under}${part.name}.` },
2244
2570
  // Its ways out are the record's: through this form's check.
2245
- { letGo, close, leaveNow: (how) => atOnce(how) });
2571
+ { letGo, close, leaveNow: (how) => atOnce(how), recordShown: shownNow });
2246
2572
  attached.set(id, { name: part.name, inner: made });
2247
2573
  // A change inside it is told as a change of its answers here, whoever made it.
2248
2574
  made.form.on('change', ({ by }) => {