@fieldia/core 0.23.0 → 0.25.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 (221) hide show
  1. package/README.md +211 -3
  2. package/package.json +1 -1
  3. package/schema/page.schema.json +1051 -325
  4. package/src/index.d.mts +9 -2
  5. package/src/index.d.ts +9 -2
  6. package/src/index.js +31 -1
  7. package/src/index.js.map +1 -1
  8. package/src/index.mjs +12 -2
  9. package/src/index.mjs.map +1 -1
  10. package/src/lib/format/actions.d.mts +31 -1
  11. package/src/lib/format/actions.d.ts +31 -1
  12. package/src/lib/format/actions.js +1 -0
  13. package/src/lib/format/actions.js.map +1 -1
  14. package/src/lib/format/actions.mjs +1 -0
  15. package/src/lib/format/actions.mjs.map +1 -1
  16. package/src/lib/format/autofill-group.d.mts +12 -0
  17. package/src/lib/format/autofill-group.d.ts +12 -0
  18. package/src/lib/format/autofill-group.js +23 -0
  19. package/src/lib/format/autofill-group.js.map +1 -0
  20. package/src/lib/format/autofill-group.mjs +19 -0
  21. package/src/lib/format/autofill-group.mjs.map +1 -0
  22. package/src/lib/format/autofill.d.mts +14 -0
  23. package/src/lib/format/autofill.d.ts +14 -0
  24. package/src/lib/format/autofill.js +54 -0
  25. package/src/lib/format/autofill.js.map +1 -0
  26. package/src/lib/format/autofill.mjs +48 -0
  27. package/src/lib/format/autofill.mjs.map +1 -0
  28. package/src/lib/format/bulk-edit.d.mts +18 -0
  29. package/src/lib/format/bulk-edit.d.ts +18 -0
  30. package/src/lib/format/bulk-edit.js +26 -0
  31. package/src/lib/format/bulk-edit.js.map +1 -0
  32. package/src/lib/format/bulk-edit.mjs +21 -0
  33. package/src/lib/format/bulk-edit.mjs.map +1 -0
  34. package/src/lib/format/duplicate-kinds.d.mts +10 -0
  35. package/src/lib/format/duplicate-kinds.d.ts +10 -0
  36. package/src/lib/format/duplicate-kinds.js +14 -0
  37. package/src/lib/format/duplicate-kinds.js.map +1 -0
  38. package/src/lib/format/duplicate-kinds.mjs +10 -0
  39. package/src/lib/format/duplicate-kinds.mjs.map +1 -0
  40. package/src/lib/format/duplicates.d.mts +22 -0
  41. package/src/lib/format/duplicates.d.ts +22 -0
  42. package/src/lib/format/duplicates.js +24 -0
  43. package/src/lib/format/duplicates.js.map +1 -0
  44. package/src/lib/format/duplicates.mjs +21 -0
  45. package/src/lib/format/duplicates.mjs.map +1 -0
  46. package/src/lib/format/field.d.mts +291 -34
  47. package/src/lib/format/field.d.ts +291 -34
  48. package/src/lib/format/field.js +19 -4
  49. package/src/lib/format/field.js.map +1 -1
  50. package/src/lib/format/field.mjs +17 -2
  51. package/src/lib/format/field.mjs.map +1 -1
  52. package/src/lib/format/import-fields.d.mts +18 -0
  53. package/src/lib/format/import-fields.d.ts +18 -0
  54. package/src/lib/format/import-fields.js +26 -0
  55. package/src/lib/format/import-fields.js.map +1 -0
  56. package/src/lib/format/import-fields.mjs +21 -0
  57. package/src/lib/format/import-fields.mjs.map +1 -0
  58. package/src/lib/format/layout.d.mts +7357 -3127
  59. package/src/lib/format/layout.d.ts +7357 -3127
  60. package/src/lib/format/layout.js +108 -32
  61. package/src/lib/format/layout.js.map +1 -1
  62. package/src/lib/format/layout.mjs +107 -32
  63. package/src/lib/format/layout.mjs.map +1 -1
  64. package/src/lib/format/layout.types.d.mts +5 -2
  65. package/src/lib/format/layout.types.d.ts +5 -2
  66. package/src/lib/format/page.d.mts +1080 -106
  67. package/src/lib/format/page.d.ts +1080 -106
  68. package/src/lib/format/page.js +25 -0
  69. package/src/lib/format/page.js.map +1 -1
  70. package/src/lib/format/page.mjs +26 -1
  71. package/src/lib/format/page.mjs.map +1 -1
  72. package/src/lib/format/phone.d.mts +27 -0
  73. package/src/lib/format/phone.d.ts +27 -0
  74. package/src/lib/format/phone.js +43 -0
  75. package/src/lib/format/phone.js.map +1 -0
  76. package/src/lib/format/phone.mjs +39 -0
  77. package/src/lib/format/phone.mjs.map +1 -0
  78. package/src/lib/format/references.d.mts +39 -0
  79. package/src/lib/format/references.d.ts +39 -0
  80. package/src/lib/format/references.js +263 -20
  81. package/src/lib/format/references.js.map +1 -1
  82. package/src/lib/format/references.mjs +263 -20
  83. package/src/lib/format/references.mjs.map +1 -1
  84. package/src/lib/format/slots-options.d.mts +26 -0
  85. package/src/lib/format/slots-options.d.ts +26 -0
  86. package/src/lib/format/slots-options.js +35 -0
  87. package/src/lib/format/slots-options.js.map +1 -0
  88. package/src/lib/format/slots-options.mjs +31 -0
  89. package/src/lib/format/slots-options.mjs.map +1 -0
  90. package/src/lib/format/translate.js +8 -4
  91. package/src/lib/format/translate.js.map +1 -1
  92. package/src/lib/format/translate.mjs +8 -4
  93. package/src/lib/format/translate.mjs.map +1 -1
  94. package/src/lib/format/upgrade.d.mts +8 -3
  95. package/src/lib/format/upgrade.d.ts +8 -3
  96. package/src/lib/format/upgrade.js +8 -3
  97. package/src/lib/format/upgrade.js.map +1 -1
  98. package/src/lib/format/upgrade.mjs +8 -3
  99. package/src/lib/format/upgrade.mjs.map +1 -1
  100. package/src/lib/format/version.d.mts +2 -2
  101. package/src/lib/format/version.d.ts +2 -2
  102. package/src/lib/format/version.js +2 -2
  103. package/src/lib/format/version.js.map +1 -1
  104. package/src/lib/format/version.mjs +2 -2
  105. package/src/lib/format/version.mjs.map +1 -1
  106. package/src/lib/format/zoned.d.mts +8 -0
  107. package/src/lib/format/zoned.d.ts +8 -0
  108. package/src/lib/format/zoned.js +36 -0
  109. package/src/lib/format/zoned.js.map +1 -0
  110. package/src/lib/format/zoned.mjs +33 -0
  111. package/src/lib/format/zoned.mjs.map +1 -0
  112. package/src/lib/record/check.d.mts +6 -1
  113. package/src/lib/record/check.d.ts +6 -1
  114. package/src/lib/record/check.js +12 -4
  115. package/src/lib/record/check.js.map +1 -1
  116. package/src/lib/record/check.mjs +12 -4
  117. package/src/lib/record/check.mjs.map +1 -1
  118. package/src/lib/record/data-source.d.mts +309 -3
  119. package/src/lib/record/data-source.d.ts +309 -3
  120. package/src/lib/record/data-source.js +16 -0
  121. package/src/lib/record/data-source.js.map +1 -1
  122. package/src/lib/record/data-source.mjs +15 -0
  123. package/src/lib/record/data-source.mjs.map +1 -1
  124. package/src/lib/record/form.d.mts +116 -25
  125. package/src/lib/record/form.d.ts +116 -25
  126. package/src/lib/record/form.js +457 -78
  127. package/src/lib/record/form.js.map +1 -1
  128. package/src/lib/record/form.mjs +457 -78
  129. package/src/lib/record/form.mjs.map +1 -1
  130. package/src/lib/record/input-rules.d.mts +51 -9
  131. package/src/lib/record/input-rules.d.ts +51 -9
  132. package/src/lib/record/input-rules.js +119 -20
  133. package/src/lib/record/input-rules.js.map +1 -1
  134. package/src/lib/record/input-rules.mjs +118 -19
  135. package/src/lib/record/input-rules.mjs.map +1 -1
  136. package/src/lib/record/inputs.js +4 -2
  137. package/src/lib/record/inputs.js.map +1 -1
  138. package/src/lib/record/inputs.mjs +4 -2
  139. package/src/lib/record/inputs.mjs.map +1 -1
  140. package/src/lib/record/limits.d.mts +1 -1
  141. package/src/lib/record/limits.d.ts +1 -1
  142. package/src/lib/record/limits.js +4 -2
  143. package/src/lib/record/limits.js.map +1 -1
  144. package/src/lib/record/limits.mjs +4 -2
  145. package/src/lib/record/limits.mjs.map +1 -1
  146. package/src/lib/record/locales/ar.js +3 -2
  147. package/src/lib/record/locales/ar.js.map +1 -1
  148. package/src/lib/record/locales/ar.mjs +3 -2
  149. package/src/lib/record/locales/ar.mjs.map +1 -1
  150. package/src/lib/record/locales/de.js +3 -2
  151. package/src/lib/record/locales/de.js.map +1 -1
  152. package/src/lib/record/locales/de.mjs +3 -2
  153. package/src/lib/record/locales/de.mjs.map +1 -1
  154. package/src/lib/record/locales/fr.js +3 -2
  155. package/src/lib/record/locales/fr.js.map +1 -1
  156. package/src/lib/record/locales/fr.mjs +3 -2
  157. package/src/lib/record/locales/fr.mjs.map +1 -1
  158. package/src/lib/record/memory-data-source.d.mts +20 -2
  159. package/src/lib/record/memory-data-source.d.ts +20 -2
  160. package/src/lib/record/memory-data-source.js +153 -35
  161. package/src/lib/record/memory-data-source.js.map +1 -1
  162. package/src/lib/record/memory-data-source.mjs +154 -36
  163. package/src/lib/record/memory-data-source.mjs.map +1 -1
  164. package/src/lib/record/memory-duplicates.d.mts +19 -0
  165. package/src/lib/record/memory-duplicates.d.ts +19 -0
  166. package/src/lib/record/memory-duplicates.js +78 -0
  167. package/src/lib/record/memory-duplicates.js.map +1 -0
  168. package/src/lib/record/memory-duplicates.mjs +74 -0
  169. package/src/lib/record/memory-duplicates.mjs.map +1 -0
  170. package/src/lib/record/memory-schedules.d.mts +45 -0
  171. package/src/lib/record/memory-schedules.d.ts +45 -0
  172. package/src/lib/record/memory-schedules.js +81 -0
  173. package/src/lib/record/memory-schedules.js.map +1 -0
  174. package/src/lib/record/memory-schedules.mjs +78 -0
  175. package/src/lib/record/memory-schedules.mjs.map +1 -0
  176. package/src/lib/record/messages.d.mts +3 -1
  177. package/src/lib/record/messages.d.ts +3 -1
  178. package/src/lib/record/messages.js +3 -2
  179. package/src/lib/record/messages.js.map +1 -1
  180. package/src/lib/record/messages.mjs +3 -2
  181. package/src/lib/record/messages.mjs.map +1 -1
  182. package/src/lib/record/periods.d.mts +47 -0
  183. package/src/lib/record/periods.d.ts +47 -0
  184. package/src/lib/record/periods.js +86 -0
  185. package/src/lib/record/periods.js.map +1 -0
  186. package/src/lib/record/periods.mjs +79 -0
  187. package/src/lib/record/periods.mjs.map +1 -0
  188. package/src/lib/record/rules.js +5 -6
  189. package/src/lib/record/rules.js.map +1 -1
  190. package/src/lib/record/rules.mjs +5 -6
  191. package/src/lib/record/rules.mjs.map +1 -1
  192. package/src/lib/record/run.d.mts +75 -9
  193. package/src/lib/record/run.d.ts +75 -9
  194. package/src/lib/record/run.js +169 -26
  195. package/src/lib/record/run.js.map +1 -1
  196. package/src/lib/record/run.mjs +170 -27
  197. package/src/lib/record/run.mjs.map +1 -1
  198. package/src/lib/record/search.d.mts +13 -3
  199. package/src/lib/record/search.d.ts +13 -3
  200. package/src/lib/record/search.js +11 -4
  201. package/src/lib/record/search.js.map +1 -1
  202. package/src/lib/record/search.mjs +11 -4
  203. package/src/lib/record/search.mjs.map +1 -1
  204. package/src/lib/record/waits.d.mts +32 -0
  205. package/src/lib/record/waits.d.ts +32 -0
  206. package/src/lib/record/waits.js +89 -0
  207. package/src/lib/record/waits.js.map +1 -0
  208. package/src/lib/record/waits.mjs +85 -0
  209. package/src/lib/record/waits.mjs.map +1 -0
  210. package/src/lib/record/zones.d.mts +57 -0
  211. package/src/lib/record/zones.d.ts +57 -0
  212. package/src/lib/record/zones.js +147 -0
  213. package/src/lib/record/zones.js.map +1 -0
  214. package/src/lib/record/zones.mjs +132 -0
  215. package/src/lib/record/zones.mjs.map +1 -0
  216. package/src/runtime.d.mts +19 -9
  217. package/src/runtime.d.ts +19 -9
  218. package/src/runtime.js +37 -2
  219. package/src/runtime.js.map +1 -1
  220. package/src/runtime.mjs +9 -2
  221. 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");
@@ -19,6 +20,7 @@ const roles_1 = require("./roles");
19
20
  const cells_1 = require("./cells");
20
21
  const messages_1 = require("./messages");
21
22
  const data_source_1 = require("./data-source");
23
+ const waits_1 = require("./waits");
22
24
  const uploads_1 = require("./uploads");
23
25
  const scheduler_1 = require("./scheduler");
24
26
  const run_1 = require("./run");
@@ -34,15 +36,23 @@ function unsavedWork(state) {
34
36
  }
35
37
  /**
36
38
  * 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.
39
+ * change of theirs the last save did not send, or text they left since that
40
+ * does not read ("12a" counts as "12" does: `unsent`), files still on
41
+ * their way, changes Reload set aside, the changes of a save refused for a
42
+ * conflict, waiting for Reload or Keep mine (only a source that keeps
43
+ * versions has one) — and, `how.changes`, any change not saved. What
44
+ * `form.leave()` — the one check every way out goes through — asks about,
45
+ * once the save on its way and the files on theirs settled. Before a
46
+ * record's first save, a change not saved is not at risk: as in 0.20, each
47
+ * way out decides.
43
48
  */
44
49
  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));
50
+ return ((state.unsent ?? []).length > 0 ||
51
+ state.setAside != null ||
52
+ (state.uploads ?? []).some(uploads_1.uploadGoing) ||
53
+ // The person asked for these to be saved, and someone else's save came first: still theirs, and not saved.
54
+ (state.conflict != null && state.dirty.length > 0) ||
55
+ (how.changes === true && (state.dirty.length > 0 || Object.keys(state.unread ?? {}).length > 0)));
46
56
  }
47
57
  /**
48
58
  * The record state behind a page: values, what changed, what is visible and
@@ -54,6 +64,8 @@ function createForm(options) {
54
64
  const page = (0, upgrade_1.upgradePage)(options.page);
55
65
  return innerForm(page === options.page ? options : { ...options, page }, []).form;
56
66
  }
67
+ /** Whether a run works a person's change out: a page's `change` steps, set off by it, or a run they set off. */
68
+ const worksOut = (chain) => chain.some((key) => key.startsWith('on.change.'));
57
69
  /** A line's cell whose line is no longer there. */
58
70
  const gone = Symbol('gone');
59
71
  const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -66,7 +78,8 @@ const asValue = (values) => values;
66
78
  * its ways out go through that one's check.
67
79
  */
68
80
  function innerForm(options, within, place, outer) {
69
- const { page } = options;
81
+ // Its time zone written into each date and time field naming none, for whatever reads one from its field alone.
82
+ const page = (0, zoned_1.zonedPage)(options.page);
70
83
  const scheduler = options.scheduler ?? scheduler_1.hostScheduler;
71
84
  const messages = options.messages ?? messages_1.MESSAGES.en;
72
85
  const index = indexLayout(page.layout);
@@ -130,7 +143,19 @@ function innerForm(options, within, place, outer) {
130
143
  const warns = [...nodeRules.values()].some((rules) => rules.some((compiled) => compiled.rule.level === 'warning'));
131
144
  const steps = page.layout.type === 'wizard' ? page.layout.children.map((step) => step.id) : [];
132
145
  const draftKey = () => `fieldia:draft:${page.id}:${state.recordId ?? 'new'}`;
133
- const today = () => (0, functions_1.localDay)(options.now?.() ?? new Date());
146
+ /** Today, on the clock of the page's time zone (the browser's own when it names none). */
147
+ const today = () => (0, zones_1.dayIn)(options.now?.() ?? new Date(), page.timeZone);
148
+ const clock = () => (options.now?.() ?? new Date()).getTime();
149
+ /** What a page's bot check saw — its hidden box filled, the captcha's token — and when the page was shown or started again. */
150
+ let bot = { honeypot: false, captcha: null, since: clock() };
151
+ /** What the bot check saw as the answers go; none without one. */
152
+ function botReport() {
153
+ const check = page.botCheck;
154
+ if (!check)
155
+ return undefined;
156
+ const taken = Math.max(0, clock() - bot.since) / 1000;
157
+ return { honeypot: check.honeypot !== false && bot.honeypot, tooFast: taken < (check.minSeconds ?? 3), seconds: Math.round(taken * 10) / 10, ...(check.captcha ? { captcha: bot.captcha } : {}) };
158
+ }
134
159
  // 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
160
  let started = false;
136
161
  let editing = options.editing !== false;
@@ -140,6 +165,8 @@ function innerForm(options, within, place, outer) {
140
165
  const editingNow = () => editing && !(started && state.can?.edit === false);
141
166
  const about = () => ({ recordId: started ? state.recordId : (options.recordId ?? null), user: options.user, editing: editingNow(), context: options.context, parent: parentContext });
142
167
  const computed = (0, compute_1.compileComputed)(page, today, about);
168
+ /** Each value worked out from others — by the page, or by the app as the lines change — and what it waits for. */
169
+ const workedOut = (0, waits_1.workedOutFrom)(page, !!options.dataSource?.onchange && page.data.kind === 'record', !!options.parent);
143
170
  const setWhen = (0, set_when_1.compileSetWhen)(page, today, about);
144
171
  /** The saved forms placed on the page, by the part's id: each one's answers sit under its name, as JSON. */
145
172
  const parts = new Map();
@@ -292,16 +319,48 @@ function innerForm(options, within, place, outer) {
292
319
  let generation = 0;
293
320
  /** Closed (`dispose`): a run still going stops quietly, as its dialog or page is gone — no other record replaced it. */
294
321
  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. */
322
+ /** Counted at each reset (the person's Discard): a save on its way then lands as the record it stored. */
323
+ let resets = 0;
324
+ /**
325
+ * THE test every late answer passes (`recordShown`): taken as it is asked for, true while the record it was asked
326
+ * 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
327
+ * dialog, a record step's — none is laid on another record, nor over the values the person's Discard let go.
328
+ */
329
+ /** The data source with a member a record's page asks of it, and the page's model: refused where either is missing. */
330
+ function recordSource(member) {
331
+ const source = options.dataSource;
332
+ if (page.data.kind !== 'record' || !source?.[member])
333
+ throw new Error(`The data source has no ${member}()`);
334
+ return [source, page.data.model];
335
+ }
336
+ function shownNow() {
337
+ const at = generation;
338
+ const reset = resets;
339
+ // A saved form placed in a record is the record's: another shown round it, its answers are laid nowhere either.
340
+ const round = outer?.recordShown();
341
+ return () => !disposed && at === generation && reset === resets && (!round || round());
342
+ }
343
+ /**
344
+ * The person's claims on the record: the fields whose last write was theirs. A step's, a rule's or the app's
345
+ * write of the whole field takes one over; a write that works the person's change out (the source's onchange, the
346
+ * page's `change` steps it sets off) keeps it.
347
+ */
296
348
  const personal = new Set();
349
+ /**
350
+ * Of a table the person did not write whole: the keys of the lines they changed, added or took away. A line a
351
+ * step adds is none of theirs, and leaves theirs as they are.
352
+ */
353
+ const personalLines = new Map();
297
354
  /** Fields where a file the person picked landed from the store: at risk until a save sends it, a first save or not. */
298
355
  const filed = new Set();
299
356
  /** 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
357
  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;
358
+ /**
359
+ * The records being deleted, by the person's choice, each by its generation: a way out of one of them lets their
360
+ * work in it go, unasked. Another record shown meanwhile is asked about as any.
361
+ */
362
+ const deleting = new Map();
363
+ const deletingShown = () => (deleting.get(generation) ?? 0) > 0;
305
364
  /** Each field's last comparison with what was sent, so a value that did not move is not compared again. */
306
365
  const compared = new Map();
307
366
  /** Fields showing a file that did not reach the store: shown until it is sent again or taken away, whatever else changes. */
@@ -439,8 +498,9 @@ function innerForm(options, within, place, outer) {
439
498
  const was = state.values;
440
499
  const shown = state.errors;
441
500
  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) {
501
+ // The person's changes the last save did not send, worked out again from the values as they are now, and from the
502
+ // text they left that does not read.
503
+ if (patch.unsent === undefined && (patch.values !== undefined || patch.unread !== undefined)) {
444
504
  const unsent = unsentNow();
445
505
  if (!sameList(unsent, state.unsent))
446
506
  state = { ...state, unsent };
@@ -469,42 +529,66 @@ function innerForm(options, within, place, outer) {
469
529
  for (const listener of [...heard])
470
530
  listener(payload);
471
531
  }
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);
532
+ /**
533
+ * A field written: told to the app, and — a person's change — the page's `change` steps for it run. `claim` says
534
+ * what of the person's it touches: `line`, the key of the one line of a table the person changed; `keep`, a write
535
+ * that leaves the person's claims as they are (a line a step adds, a write working their change out).
536
+ */
537
+ function changed(field, by, claim = {}) {
538
+ noteOrigin(field, by, claim);
475
539
  emit('change', { field, value: state.values[field], values: state.values, by });
476
540
  const list = page.on?.change?.[field];
477
541
  if (by === 'person' && list)
478
542
  void track(moment(`on.change.${field}`, list, [], true));
479
543
  }
480
- /** Another record is shown: nothing of the last one's is the person's work any more. */
544
+ /** 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
545
  function newGeneration() {
482
546
  generation++;
483
547
  personal.clear();
548
+ personalLines.clear();
484
549
  filed.clear();
485
550
  compared.clear();
486
551
  sentAs = null;
552
+ clearFiles();
487
553
  }
488
554
  /** 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
555
  const filedIn = (name) => filed.has(name) || [...attached.values()].some((part) => part.name === name && part.inner.files.filed());
490
556
  /**
491
557
  * The person's work no save took, by field, in the page's order: each of
492
558
  * their changes that differs from the record as the last save left it —
493
- * before a first save, only a file the store holds.
559
+ * a field they wrote, or a line of a table they changed — and text they
560
+ * left in a box of it that does not read (`unread`), which no save can
561
+ * send; before a first save, only a file the store holds.
494
562
  */
495
563
  function unsentNow() {
496
- if (!personal.size)
564
+ const typed = sentAs ? unreadFields() : null;
565
+ if (!personal.size && !personalLines.size && !typed?.size)
497
566
  return [];
498
567
  const out = [];
499
568
  for (const name of Object.keys(fields)) {
500
- if (!personal.has(name))
569
+ // Typed and left unread since a save: the person's, as a change is — a value changed to "12" is, so is "12a".
570
+ if (typed?.has(name)) {
571
+ out.push(name);
572
+ continue;
573
+ }
574
+ const lines = personalLines.get(name);
575
+ if (!personal.has(name) && !lines)
501
576
  continue;
502
577
  const from = sentAs ?? (filedIn(name) ? baseline : null);
503
- if (from && differs(name, state.values[name], from[name]))
578
+ if (!from)
579
+ continue;
580
+ if (lines ? linesDiffer(name, lines, state.values[name], from[name]) : differs(name, state.values[name], from[name]))
504
581
  out.push(name);
505
582
  }
506
583
  return out;
507
584
  }
585
+ /** The fields with text left unread in a box of theirs: a field's, a line's cell's, a property's. */
586
+ function unreadFields() {
587
+ const out = new Set();
588
+ for (const path of Object.keys(state.unread ?? {}))
589
+ out.add(path.split('.')[0]);
590
+ return out;
591
+ }
508
592
  function differs(name, value, sent) {
509
593
  const known = compared.get(name);
510
594
  if (known && known.value === value && known.sent === sent)
@@ -513,18 +597,53 @@ function innerForm(options, within, place, outer) {
513
597
  compared.set(name, { value, sent, differs: verdict });
514
598
  return verdict;
515
599
  }
600
+ /** Whether any of these lines of a table differs from the table as sent: changed, added, or taken away. */
601
+ function linesDiffer(name, keys, value, sent) {
602
+ const def = fields[name];
603
+ const find = (lines, key) => (lines ?? []).find((line) => line.key === key);
604
+ for (const key of keys) {
605
+ const now = find(value, key);
606
+ const was = find(sent, key);
607
+ if (!now && !was)
608
+ continue;
609
+ if (!now || !was)
610
+ return true;
611
+ if (now.values === was.values)
612
+ continue;
613
+ for (const sub of new Set([...Object.keys(now.values), ...Object.keys(was.values)])) {
614
+ if (!sameValue(def.fields[sub], now.values[sub], was.values[sub]))
615
+ return true;
616
+ }
617
+ }
618
+ return false;
619
+ }
516
620
  /** `unsent` as it is now, told when it changed. */
517
621
  function refreshUnsent() {
518
622
  const unsent = unsentNow();
519
623
  if (!sameList(unsent, state.unsent))
520
624
  set({ unsent });
521
625
  }
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
626
+ /**
627
+ * A field written, and by whom: the person's change is theirs — the field, or the line of a table they changed —
628
+ * until a step, a rule or the app writes the whole field in its place. A write that leaves their change where it
629
+ * is (`keep`: a line a step adds, a `change` step working out what they typed) leaves it theirs.
630
+ */
631
+ function noteOrigin(field, by, claim) {
632
+ if (by === 'person') {
633
+ if (claim.line === undefined) {
634
+ personal.add(field);
635
+ personalLines.delete(field);
636
+ }
637
+ else if (!personal.has(field)) {
638
+ const keys = personalLines.get(field) ?? new Set();
639
+ keys.add(claim.line);
640
+ personalLines.set(field, keys);
641
+ }
642
+ }
643
+ else if (!claim.keep) {
527
644
  personal.delete(field);
645
+ personalLines.delete(field);
646
+ }
528
647
  refreshUnsent();
529
648
  }
530
649
  /** A wizard step entered: told to the app, and its `show` steps run. */
@@ -694,10 +813,12 @@ function innerForm(options, within, place, outer) {
694
813
  const ticks = node.source.widget === 'tick' && page.fields[name].type === 'boolean';
695
814
  return ticks && state.values[name] !== true && nodeState(node.id).required ? messages.tick : undefined;
696
815
  }
816
+ /** The widget a field is shown with where it keeps its value its own way (a per cent): the first of its nodes'. */
817
+ const shownWidget = (shown) => shown.map((node) => node.source.widget).find((widget) => widget === 'percentage');
697
818
  /** A field node's error now: its field's own rules first, then its answer rules. */
698
819
  function nodeError(node) {
699
820
  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;
821
+ 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
822
  }
702
823
  /** Warnings for the visible fields of the page, without publishing them. */
703
824
  function collectWarnings() {
@@ -781,7 +902,7 @@ function innerForm(options, within, place, outer) {
781
902
  const cell = ruled[sub];
782
903
  if (cell?.invisible || (tables.has(node.id) && tables.get(node.id).compiled.hidden(sub, reading().context, reading().env, userRoles)))
783
904
  continue;
784
- const lineMessage = (0, check_1.checkValue)(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today());
905
+ const lineMessage = (0, check_1.checkValue)(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today(), node.source.cells?.[sub]?.widget);
785
906
  if (lineMessage)
786
907
  errors[`${name}.${line.key}.${sub}`] = lineMessage;
787
908
  }
@@ -846,7 +967,7 @@ function innerForm(options, within, place, outer) {
846
967
  if ((0, values_1.isEmpty)(fieldDef(field), value))
847
968
  return undefined;
848
969
  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())) ??
970
+ besidesRows(field, (0, check_1.checkValue)(checked(field), value, false, messages, today(), shownWidget(shown))) ??
850
971
  shown.map((node) => ruleCheck(node).error).find(Boolean));
851
972
  }
852
973
  if (parts.length === 2 && page.fields[field]?.type === 'properties')
@@ -1032,8 +1153,10 @@ function innerForm(options, within, place, outer) {
1032
1153
  return;
1033
1154
  const seq = ++onchangeSeq;
1034
1155
  const request = { model: page.data.model, id: state.recordId, changed, values: (0, values_1.structuredCopy)(state.values) };
1156
+ const shown = shownNow();
1035
1157
  void track(source.onchange(request).then((result) => {
1036
- if (seq !== onchangeSeq)
1158
+ // A newer change asked since; or the record it was asked for is gone, or its values let go: none of it is laid.
1159
+ if (seq !== onchangeSeq || !shown())
1037
1160
  return;
1038
1161
  const values = { ...state.values, ...(result.values ?? {}) };
1039
1162
  writeValues(values, { warning: result.warning ?? null, warningField: result.warning ? changed : null });
@@ -1134,18 +1257,27 @@ function innerForm(options, within, place, outer) {
1134
1257
  set({ status: 'error', error: problem.message, saveProblem: problem, conflict: null });
1135
1258
  return { reason: 'save', message: problem.message };
1136
1259
  }
1260
+ // Asked for the record shown now: another shown while it waits below, it saves nothing of that one, nor says anything there.
1261
+ const asked = generation;
1262
+ const elsewhere = () => ({ reason: 'save', message: messages.recordReplaced });
1137
1263
  // 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
1264
  // up is waited for, so the record it names is what is saved — or, named by none, why nothing is.
1139
1265
  const waiting = readTyped();
1140
1266
  if (waiting)
1141
1267
  await waiting;
1268
+ if (asked !== generation)
1269
+ return elsewhere();
1142
1270
  // 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
1271
  if (filesGoing()) {
1144
1272
  set({ status: 'saving', error: null, saveProblem: null });
1145
1273
  await filesSettled();
1274
+ if (asked !== generation)
1275
+ return elsewhere();
1146
1276
  set({ status: 'ready' });
1147
1277
  }
1148
1278
  if (page.on?.beforeSave) {
1279
+ // Its run stops of itself on a record shown by anything but its own steps (a `call`'s `{ record }`, a reload):
1280
+ // those move the save on with it.
1149
1281
  const before = await moment('on.beforeSave', page.on.beforeSave, chain, false);
1150
1282
  if (!before.done) {
1151
1283
  // A check's problems, or an inner save's, are shown already; any other stop is this save's problem.
@@ -1170,6 +1302,8 @@ function innerForm(options, within, place, outer) {
1170
1302
  try {
1171
1303
  // 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
1304
  await storeBytes(page.data.kind === 'responses' || state.recordId == null ? Object.keys(fields) : state.dirty);
1305
+ if (at !== generation)
1306
+ return elsewhere();
1173
1307
  const failed = fileProblems();
1174
1308
  if (Object.keys(failed).length) {
1175
1309
  set({ errors: failed, ...unstored(failed) });
@@ -1181,7 +1315,11 @@ function innerForm(options, within, place, outer) {
1181
1315
  const sent = shownValues();
1182
1316
  const before = state.values;
1183
1317
  at = generation;
1184
- await source.submit({ pageId: page.id, values: sent });
1318
+ const seen = botReport();
1319
+ await source.submit({ pageId: page.id, values: sent, ...(seen ? { bot: seen } : {}) });
1320
+ // The captcha's token is spent once sent: the next send waits for a new one.
1321
+ if (seen?.captcha)
1322
+ bot.captcha = null;
1185
1323
  // Answered for answers no longer shown — the form was started again meanwhile: the source has them, the form lays nothing.
1186
1324
  if (at !== generation)
1187
1325
  return null;
@@ -1189,7 +1327,7 @@ function innerForm(options, within, place, outer) {
1189
1327
  // What it sent is what an answer changed since is told against: still to send.
1190
1328
  sentAs = before;
1191
1329
  set({ status: 'saved', unsent: unsentNow() });
1192
- emit('send', { values: sent });
1330
+ emit('send', { values: sent, ...(seen ? { bot: seen } : {}) });
1193
1331
  void track(moment('on.afterSave', page.on?.afterSave, chain, false));
1194
1332
  return null;
1195
1333
  }
@@ -1225,7 +1363,8 @@ function innerForm(options, within, place, outer) {
1225
1363
  scheduleDraft();
1226
1364
  scheduleAutosave();
1227
1365
  }
1228
- emit('save', { recordId: state.recordId, values: state.values });
1366
+ // The record as this save left it: a change made while it was on its way is no value of it.
1367
+ emit('save', { recordId: state.recordId, values: baseline });
1229
1368
  void track(moment('on.afterSave', page.on?.afterSave, chain, false));
1230
1369
  return null;
1231
1370
  }
@@ -1281,8 +1420,12 @@ function innerForm(options, within, place, outer) {
1281
1420
  }
1282
1421
  /** The form has its values: told to the app, its `open` steps run, then the wizard step it shows is told. */
1283
1422
  async function opening() {
1423
+ const at = generation;
1284
1424
  emit('open', { recordId: state.recordId, values: state.values });
1285
1425
  await moment('on.open', page.on?.open, [], true);
1426
+ // Another record shown while its open steps ran: that one's opening tells its own steps.
1427
+ if (at !== generation)
1428
+ return;
1286
1429
  showing = true;
1287
1430
  tellStep([]);
1288
1431
  }
@@ -1303,8 +1446,8 @@ function innerForm(options, within, place, outer) {
1303
1446
  for (const name of names)
1304
1447
  changed(name, 'app');
1305
1448
  }
1306
- /** Write fields — a step's or the app's — worked out once, and tell each. */
1307
- function write(written, by) {
1449
+ /** Write fields — a step's or the app's — worked out once, and tell each; `keep`, a write working the person's change out. */
1450
+ function write(written, by, keep = false) {
1308
1451
  const names = Object.keys(written);
1309
1452
  names.forEach(fieldDef);
1310
1453
  if (!names.length)
@@ -1313,8 +1456,9 @@ function innerForm(options, within, place, outer) {
1313
1456
  for (const name of names)
1314
1457
  afterEdit(name);
1315
1458
  for (const name of names)
1316
- changed(name, by);
1459
+ changed(name, by, { keep });
1317
1460
  }
1461
+ /** A line added: the person's, theirs; a step's, none of theirs, and the lines they changed stay theirs. */
1318
1462
  function addLine(field, values, by) {
1319
1463
  const def = lineField(field);
1320
1464
  const current = state.values[field] ?? [];
@@ -1337,7 +1481,7 @@ function innerForm(options, within, place, outer) {
1337
1481
  const lines = [...current, line];
1338
1482
  writeValues({ ...state.values, [field]: lines });
1339
1483
  afterEdit(field);
1340
- changed(field, by);
1484
+ changed(field, by, by === 'person' ? { line: key } : { keep: true });
1341
1485
  return key;
1342
1486
  }
1343
1487
  /**
@@ -1346,15 +1490,44 @@ function innerForm(options, within, place, outer) {
1346
1490
  */
1347
1491
  async function check(names) {
1348
1492
  names?.forEach(fieldDef);
1493
+ const at = generation;
1349
1494
  const waiting = readTyped();
1350
1495
  if (waiting)
1351
1496
  await waiting;
1497
+ // Another record shown while it waited: nothing found is shown on that one.
1498
+ if (at !== generation)
1499
+ return replaced();
1352
1500
  const all = collectErrors();
1353
1501
  const found = names ? Object.fromEntries(Object.entries(all).filter(([key]) => names.some((name) => key === name || key.startsWith(`${name}.`)))) : all;
1354
1502
  set({ errors: found });
1355
1503
  const first = Object.values(found)[0];
1356
1504
  return first === undefined ? null : { reason: 'check', message: first };
1357
1505
  }
1506
+ /**
1507
+ * What is typed read first, for a run the person or the app set off (`RunScope.typed`): every box read as a save
1508
+ * reads it, a name looked up waited for. Text left that does not read stops the run, said under its box as a check
1509
+ * says it — never a value under it handed on or worked from.
1510
+ */
1511
+ async function typedFirst() {
1512
+ const at = generation;
1513
+ const waiting = readTyped();
1514
+ if (waiting)
1515
+ await waiting;
1516
+ // Another record shown while it waited: the run is not for that one.
1517
+ if (at !== generation)
1518
+ return replaced();
1519
+ const found = unreadProblems();
1520
+ const first = Object.values(found)[0];
1521
+ if (first === undefined)
1522
+ return null;
1523
+ set({ errors: { ...state.errors, ...found } });
1524
+ return { reason: 'check', message: first };
1525
+ }
1526
+ /** The problems of text left unread in boxes shown — this form's and its saved forms' placed here — by path. */
1527
+ function unreadProblems() {
1528
+ const inner = (path) => [...attached.values()].some((part) => path.startsWith(`${part.name}.`) && !!part.inner.form.getState().unread?.[path.slice(part.name.length + 1)]);
1529
+ return Object.fromEntries(Object.entries(collectErrors()).filter(([path]) => !!state.unread[path] || inner(path)));
1530
+ }
1358
1531
  /** Go to a wizard's step for a run; undefined when the target is no step of it, but a tab. */
1359
1532
  async function goToStep(target, chain) {
1360
1533
  if (!steps.includes(target))
@@ -1362,16 +1535,19 @@ function innerForm(options, within, place, outer) {
1362
1535
  if (!form.steps().includes(target))
1363
1536
  return { reason: 'cannot', message: `The step "${target}" does not apply now` };
1364
1537
  // A name still being looked up in a box on the way is waited for, as a save waits for it.
1538
+ const at = generation;
1365
1539
  const waiting = readTyped();
1366
1540
  if (waiting)
1367
1541
  await waiting;
1542
+ if (at !== generation)
1543
+ return replaced();
1368
1544
  let went = false;
1369
1545
  inRun(chain, () => (went = form.goTo(target)));
1370
1546
  return went ? null : { reason: 'check', message: Object.values(state.errors)[0] };
1371
1547
  }
1372
1548
  /** 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
1549
  async function runTop(list, scope) {
1374
- scope.generation = generation;
1550
+ scope.generation ??= generation;
1375
1551
  const result = await runner.runSteps(list, scope);
1376
1552
  emit('run', { id: scope.id, steps: list, result });
1377
1553
  return result;
@@ -1389,7 +1565,7 @@ function innerForm(options, within, place, outer) {
1389
1565
  throw new Error(`"${name}" cannot make records: its data source has no create, or it is not a link`);
1390
1566
  return track(source.create({ model, name: typed.trim(), ...(Object.keys(values).length ? { values } : {}) }));
1391
1567
  }
1392
- async function runSearch(def, name, ctx, query, limit, model, ids) {
1568
+ async function runSearch(def, name, ctx, query, limit, { model, ids, exact } = {}) {
1393
1569
  // Formatted text names people and records in place (@mention, #4127): searched by the model they are of.
1394
1570
  const byModel = def.type === 'reference' || def.type === 'json' || def.type === 'html';
1395
1571
  if (def.type !== 'many2one' && def.type !== 'many2many' && !byModel) {
@@ -1404,7 +1580,7 @@ function innerForm(options, within, place, outer) {
1404
1580
  const own = byModel || !def.filter ? undefined : (0, filter_1.resolveFilter)(def.filter, ctx);
1405
1581
  const filter = ids ? [...(own ?? []), { field: 'id', op: 'in', value: ids }] : own;
1406
1582
  const relation = byModel ? model : def.relation;
1407
- return track(source.search({ model: relation, query, ...(filter ? { filter } : {}), limit }));
1583
+ return track(source.search({ model: relation, query, ...(filter ? { filter } : {}), limit, ...(exact ? { exact: true } : {}) }));
1408
1584
  }
1409
1585
  function lineField(field) {
1410
1586
  const def = fieldDef(field);
@@ -1505,7 +1681,7 @@ function innerForm(options, within, place, outer) {
1505
1681
  }
1506
1682
  }
1507
1683
  /** 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));
1684
+ const atOnce = (how, own = false) => !deletingShown() && !checking && !busy(own) && (how.discard === true || !workAtRisk(state, how));
1509
1685
  /** The saves on their way, settled: what a load that is not a way out (a conflict's Reload) waits for. */
1510
1686
  async function savesSettled() {
1511
1687
  while (saveRunning)
@@ -1531,10 +1707,13 @@ function innerForm(options, within, place, outer) {
1531
1707
  if (outer)
1532
1708
  return outer.letGo(how, chain);
1533
1709
  // A delete the person chose: their work goes with the record, unasked.
1534
- if (deletes) {
1710
+ if (deletingShown()) {
1535
1711
  forget();
1536
1712
  return true;
1537
1713
  }
1714
+ // The person's own Discard stops the files on their way, never waits for them.
1715
+ if (how.discard)
1716
+ clearFiles();
1538
1717
  const own = chain.includes('on.beforeSave');
1539
1718
  if (atOnce(how, own)) {
1540
1719
  if (how.discard)
@@ -1577,13 +1756,46 @@ function innerForm(options, within, place, outer) {
1577
1756
  forget();
1578
1757
  return yes;
1579
1758
  }
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. */
1759
+ /**
1760
+ * Let go of the person's work at risk: files on their way stop, changes set aside go, a conflict's changes are
1761
+ * asked about no more, and the changes no save took go back to the record as the last save left it — so what a
1762
+ * dialog or a page in place hands back once it saved is the record as saved, never the change let go.
1763
+ */
1581
1764
  function forget() {
1765
+ const back = unsentLetGo();
1582
1766
  clearFiles();
1583
1767
  personal.clear();
1768
+ personalLines.clear();
1584
1769
  filed.clear();
1585
- if (state.setAside !== null || state.unsent.length)
1586
- set({ setAside: null, unsent: [] });
1770
+ if (back)
1771
+ writeValues({ ...state.values, ...back });
1772
+ // Text left that does not read goes too: each box shows what its field holds.
1773
+ const unread = Object.keys(state.unread).length ? { unread: {} } : {};
1774
+ if (state.setAside !== null || state.unsent.length || state.conflict !== null || unread.unread)
1775
+ set({ setAside: null, unsent: [], conflict: null, ...unread });
1776
+ }
1777
+ /** The fields holding the person's changes no save took, as the record as last saved holds them: their lines alone, for a table. */
1778
+ function unsentLetGo() {
1779
+ if (!state.unsent.length)
1780
+ return null;
1781
+ const out = {};
1782
+ for (const name of state.unsent) {
1783
+ const from = sentAs ?? baseline;
1784
+ const keys = personalLines.get(name);
1785
+ out[name] = keys && !personal.has(name) ? linesAsSent(state.values[name], from[name], keys) : (0, values_1.structuredCopy)(from[name]);
1786
+ }
1787
+ return out;
1788
+ }
1789
+ /** A table with these lines as sent: changed ones as they were, added ones gone, ones taken away back in their place. */
1790
+ function linesAsSent(value, sent, keys) {
1791
+ const was = sent ?? [];
1792
+ const of = (key) => was.find((line) => line.key === key);
1793
+ 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));
1794
+ was.forEach((line, at) => {
1795
+ if (keys.has(line.key) && !out.some((other) => other.key === line.key))
1796
+ out.splice(Math.min(at, out.length), 0, (0, values_1.structuredCopy)(line));
1797
+ });
1798
+ return out;
1587
1799
  }
1588
1800
  /** The check as a step meets it: null to go on — at once when nothing is at risk — else why the run stops. */
1589
1801
  function letGoStep(chain) {
@@ -1591,12 +1803,19 @@ function innerForm(options, within, place, outer) {
1591
1803
  return go === true ? null : go.then((yes) => (yes ? null : kept()));
1592
1804
  }
1593
1805
  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 = []) {
1806
+ /**
1807
+ * A step's own move — the record loaded again, or another shown — once the check let what is shown go: kept, the
1808
+ * person kept their work; replaced, another record was shown before it ended (the run's steps are not for that one).
1809
+ */
1810
+ async function ownMove(chain, move) {
1596
1811
  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));
1812
+ if (go !== true && !(await go))
1813
+ return kept();
1814
+ // The record it shows is a generation of its own, made as it starts: another made before it ends replaced it.
1815
+ const moving = move();
1816
+ const mine = generation;
1817
+ await moving;
1818
+ return mine === generation ? null : replaced();
1600
1819
  }
1601
1820
  /**
1602
1821
  * The host's close, as every `close` step reaches it: the form takes it
@@ -1618,6 +1837,10 @@ function innerForm(options, within, place, outer) {
1618
1837
  const go = letGo({}, chain);
1619
1838
  return go === true ? shut() : go.then((yes) => (yes ? shut() : kept()));
1620
1839
  }
1840
+ /** Why a run stops whose record was replaced while it went on — quietly, when the form was closed. */
1841
+ function replaced() {
1842
+ return disposed ? { reason: 'closed' } : { reason: 'cannot', message: messages.recordReplaced };
1843
+ }
1621
1844
  /** The record a record step asks the data source about. */
1622
1845
  const record = () => ({ model: page.data.model, id: state.recordId, fields });
1623
1846
  async function post(message) {
@@ -1640,18 +1863,30 @@ function innerForm(options, within, place, outer) {
1640
1863
  recordId: () => state.recordId,
1641
1864
  reading,
1642
1865
  today,
1643
- write: (values) => write(values, 'step'),
1866
+ // A step's write of a field takes the person's change in it over — unless the step works that change out: the
1867
+ // page's `change` steps, set off by it, as the source's onchange does.
1868
+ write: (values, chain) => write(values, 'step', worksOut(chain)),
1644
1869
  store: (values) => store(values),
1870
+ // The person's own change: the field's, or a line of it (a claim by line): put back, the whole field is theirs
1871
+ // again — at risk, asked about — never less than it was.
1872
+ mine: (field) => personal.has(field) || personalLines.has(field),
1873
+ putBack(values, mine) {
1874
+ write(values, 'step');
1875
+ for (const name of mine)
1876
+ noteOrigin(name, 'person', {});
1877
+ },
1645
1878
  addLine: (field, values) => void addLine(field, values, 'step'),
1646
1879
  check,
1880
+ typedFirst,
1647
1881
  save: saving,
1648
1882
  generation: () => generation,
1649
- gone: () => (disposed ? { reason: 'closed' } : { reason: 'cannot', message: messages.recordReplaced }),
1883
+ gone: replaced,
1650
1884
  async reload(chain) {
1651
1885
  if (state.recordId == null)
1652
1886
  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();
1887
+ const stopped = await ownMove(chain, () => load(true));
1888
+ if (stopped)
1889
+ return stopped;
1655
1890
  return state.status === 'error' ? { reason: 'cannot', message: state.error ?? 'The record could not be loaded again' } : null;
1656
1891
  },
1657
1892
  reset(chain) {
@@ -1672,24 +1907,32 @@ function innerForm(options, within, place, outer) {
1672
1907
  changed: () => state.dirty.length > 0 || state.uploads.some(uploads_1.uploadGoing),
1673
1908
  letGo: letGoStep,
1674
1909
  async deleting(remove, done) {
1675
- deletes++;
1910
+ const of = generation;
1911
+ const shown = shownNow();
1912
+ deleting.set(of, (deleting.get(of) ?? 0) + 1);
1676
1913
  try {
1677
1914
  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))
1915
+ // Deleted: what the person had not saved in it goes with it. Refused, it stays theirs. Another record shown
1916
+ // meanwhile keeps what was typed in it.
1917
+ if (done(out) && shown())
1680
1918
  forget();
1681
1919
  return out;
1682
1920
  }
1683
1921
  finally {
1684
- deletes--;
1922
+ const left = (deleting.get(of) ?? 1) - 1;
1923
+ if (left)
1924
+ deleting.set(of, left);
1925
+ else
1926
+ deleting.delete(of);
1685
1927
  }
1686
1928
  },
1687
1929
  ...(options.dataSource?.archive && page.data.kind === 'record' ? { archive: (archive) => track((options.dataSource?.archive)({ ...record(), archive })) } : {}),
1688
1930
  ...(options.dataSource?.copy && page.data.kind === 'record' ? { copy: async () => (await track((options.dataSource?.copy)(record()))).id } : {}),
1689
1931
  ...(options.dataSource?.delete && page.data.kind === 'record' ? { remove: () => track((options.dataSource?.delete)(record())) } : {}),
1690
1932
  async showRecord(id, chain) {
1691
- if (!(await openRecord(id, chain)))
1692
- return kept();
1933
+ const stopped = await ownMove(chain, () => showNow(id));
1934
+ if (stopped)
1935
+ return stopped;
1693
1936
  return state.status === 'error' ? { reason: 'cannot', message: state.error ?? 'The record could not be loaded' } : null;
1694
1937
  },
1695
1938
  told(event, id) {
@@ -1708,8 +1951,24 @@ function innerForm(options, within, place, outer) {
1708
1951
  wordsOf: (field) => wordsOfValue(fieldDef(field), state.values[field]),
1709
1952
  barred: (right) => (state.can?.[right] === false ? state.can.reason ?? messages[MAY_NOT[right]] : null),
1710
1953
  });
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`. */
1954
+ // 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
1955
+ // go when another record is shown by then (its steps were for that one).
1956
+ const moment = (0, run_1.createMoments)((key, list, chain, on) => runTop(list, { id: key, chain, generation: on }), () => generation);
1957
+ /**
1958
+ * Whether steps do something before their first save — set a status, call the app, open a page — so a record's
1959
+ * button made of them saves first. Steps that start with the save (Save and close), or only check before it, need
1960
+ * no save before them: theirs is the first.
1961
+ */
1962
+ function actsBeforeSaving(list) {
1963
+ const first = list.findIndex((step) => step.do === 'save');
1964
+ return first > 0 && list.slice(0, first).some((step) => step.do !== 'check');
1965
+ }
1966
+ /**
1967
+ * A press of a button: its confirmation first, then its steps and its app action as a final call, in `scope`. A
1968
+ * record's button whose steps act and then save saves the record first when it is new or changed, as Flectra's
1969
+ * button does: refused — a field it requires left empty — none of its steps run, so nothing it does happens to a
1970
+ * record that cannot be saved.
1971
+ */
1713
1972
  function press(source, scope) {
1714
1973
  const list = [...(source.steps ?? [])];
1715
1974
  if (source.action !== undefined)
@@ -1720,12 +1979,37 @@ function innerForm(options, within, place, outer) {
1720
1979
  list.push({ do: builtin });
1721
1980
  const asked = builtin === 'archive' ? messages.archiveConfirm : builtin === 'delete' ? messages.deleteConfirm : undefined;
1722
1981
  const confirm = 'confirm' in source && source.confirm !== undefined ? source.confirm : asked;
1982
+ // On the record it was pressed on: another shown while its question is asked, it does nothing to that one.
1983
+ scope.generation ??= generation;
1984
+ // Pressed by the person: what is typed is read before a step reads the form or hands its values on.
1985
+ scope.typed ??= 'first';
1723
1986
  return track((async () => {
1724
1987
  if (confirm && !(await runner.ask(confirm))) {
1725
1988
  const result = { done: false, reason: 'no', message: confirm };
1726
1989
  emit('run', { id: scope.id, steps: list, result });
1727
1990
  return result;
1728
1991
  }
1992
+ const saveFirst = page.data.kind === 'record' && actsBeforeSaving(list);
1993
+ // What is typed is read first, as the run reads it before its first step that reads the form (run.ts): text
1994
+ // that does not read stops it with `check`, said under its box — before the save that comes first, and any step.
1995
+ if (saveFirst && scope.typed === 'first') {
1996
+ const unread = await typedFirst();
1997
+ if (unread) {
1998
+ const at = Math.max(0, list.findIndex((step) => step.do !== 'check'));
1999
+ const result = { done: false, stoppedAt: at, step: list[at], reason: unread.reason, ...(unread.message !== undefined ? { message: unread.message } : {}) };
2000
+ emit('run', { id: scope.id, steps: list, result });
2001
+ return result;
2002
+ }
2003
+ }
2004
+ // Then the save first, then the steps.
2005
+ if (saveFirst && (state.recordId == null || state.dirty.length > 0 || Object.keys(state.unread ?? {}).length > 0 || state.uploads.some(uploads_1.uploadGoing))) {
2006
+ const first = await saving(scope.chain);
2007
+ if (first) {
2008
+ const result = { done: false, step: { do: 'save' }, reason: first.reason, ...(first.message !== undefined ? { message: first.message } : {}), ...(first.error !== undefined ? { error: first.error } : {}) };
2009
+ emit('run', { id: scope.id, steps: list, result });
2010
+ return result;
2011
+ }
2012
+ }
1729
2013
  return runTop(list, scope);
1730
2014
  })());
1731
2015
  }
@@ -1751,6 +2035,14 @@ function innerForm(options, within, place, outer) {
1751
2035
  openRecord: (id) => track(openRecord(id)),
1752
2036
  leave: (how) => Promise.resolve(letGo(how)),
1753
2037
  leaveNow: (how = {}) => (outer ? outer.leaveNow(how) : atOnce(how)),
2038
+ recordShown: shownNow,
2039
+ relatedFilter(id) {
2040
+ const node = index.get(id)?.source;
2041
+ if (node?.type !== 'related' || state.recordId == null)
2042
+ return null;
2043
+ const related = node;
2044
+ return [{ field: related.field, op: '=', value: state.recordId }, ...(related.filter ? (0, filter_1.resolveFilter)(related.filter, context()) : [])];
2045
+ },
1754
2046
  post,
1755
2047
  setEditing(next) {
1756
2048
  if (next === editing)
@@ -1763,6 +2055,10 @@ function innerForm(options, within, place, outer) {
1763
2055
  loadChoices,
1764
2056
  loadDefinitions,
1765
2057
  addDefinition,
2058
+ waitsFor(name) {
2059
+ const from = workedOut.get(name);
2060
+ return from ? (0, waits_1.waitingOn)(page, from, state.values, state.unread) : null;
2061
+ },
1766
2062
  problem(name) {
1767
2063
  fieldDef(name);
1768
2064
  // Only a field someone can see is asked anything, and required wherever it is shown so.
@@ -1779,7 +2075,7 @@ function innerForm(options, within, place, outer) {
1779
2075
  }
1780
2076
  const own = shown.map(unticked).find(Boolean) ??
1781
2077
  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());
2078
+ (0, check_1.checkValue)(checked(name), state.values[name], shown.some((node) => nodeState(node.id).required), messages, today(), shownWidget(shown));
1783
2079
  return own ?? shown.map((node) => ruleCheck(node).error).find((error) => error !== undefined) ?? null;
1784
2080
  },
1785
2081
  setUnread(path, unread) {
@@ -1825,6 +2121,12 @@ function innerForm(options, within, place, outer) {
1825
2121
  };
1826
2122
  },
1827
2123
  settle: () => readTyped() ?? Promise.resolve(),
2124
+ botCheck(seen) {
2125
+ if (seen.honeypot !== undefined)
2126
+ bot.honeypot = seen.honeypot;
2127
+ if (seen.captcha !== undefined)
2128
+ bot.captcha = seen.captcha;
2129
+ },
1828
2130
  reading: () => readingNow > 0 || [...attached.values()].some((part) => part.inner.form.reading()),
1829
2131
  validate() {
1830
2132
  readTyped();
@@ -1834,6 +2136,50 @@ function innerForm(options, within, place, outer) {
1834
2136
  },
1835
2137
  save: () => track(save()),
1836
2138
  canUpload: () => typeof options.dataSource?.upload === 'function',
2139
+ today,
2140
+ recordReads: (id, values, fields) => (0, env_1.recordContext)(values, fields ?? page.fields, { recordId: id, user: options.user, editing: false, context: options.context }),
2141
+ canFindSlots: () => typeof options.dataSource?.slots === 'function',
2142
+ findSlots(field, ask) {
2143
+ const source = options.dataSource;
2144
+ if (!source?.slots)
2145
+ return Promise.reject(new Error('The data source has no slots()'));
2146
+ const def = page.fields[field];
2147
+ const { schedule = field, length, between, from, to } = ask;
2148
+ return source.slots({
2149
+ model: page.data.kind === 'record' ? page.data.model : '',
2150
+ field,
2151
+ recordId: state.recordId,
2152
+ schedule,
2153
+ from,
2154
+ to,
2155
+ timeZone: (def?.type === 'datetime' && def.timeZone) || page.timeZone || (0, zones_1.ownZone)(),
2156
+ ...(length ? { length } : {}),
2157
+ ...(between ? { between } : {}),
2158
+ values: (0, values_1.structuredCopy)(state.values),
2159
+ });
2160
+ },
2161
+ canFindDuplicates: () => !!(page.data.kind === 'record' && page.duplicates?.fields?.length && options.dataSource?.duplicates),
2162
+ async findDuplicates() {
2163
+ const [source, model] = recordSource('duplicates');
2164
+ const fields = (page.duplicates?.fields ?? []).filter((name) => page.fields[name] && !(0, values_1.isEmpty)(page.fields[name], state.values[name]));
2165
+ const recordId = state.recordId;
2166
+ // Nothing typed in any of them: nothing asked.
2167
+ if (!fields.length)
2168
+ return [];
2169
+ const { matches } = await source.duplicates({ model, recordId, values: (0, values_1.structuredCopy)(state.values), fields });
2170
+ // Never the record itself, whatever the source says; the likest first where it scores them.
2171
+ const others = (matches ?? []).filter((match) => match && String(match.id) !== String(recordId));
2172
+ return others.every((match) => typeof match.score === 'number') ? others.sort((a, b) => b.score - a.score) : others;
2173
+ },
2174
+ canMerge: () => !!(page.data.kind === 'record' && options.dataSource?.merge && options.dataSource.load),
2175
+ async readRecord(id) {
2176
+ const [source, model] = recordSource('load');
2177
+ return (0, data_source_1.loadedRecordOf)(await source.load({ model, id, fields: page.fields })).values;
2178
+ },
2179
+ async merge(request) {
2180
+ const [source, model] = recordSource('merge');
2181
+ return source.merge({ model, keepId: request.keepId, mergeIds: [...request.mergeIds], values: (0, values_1.structuredCopy)(request.values), fields: page.fields });
2182
+ },
1837
2183
  upload: (field, files, how = {}) => uploads.start(field, files, how),
1838
2184
  uploading: (field) => state.uploads.filter((upload) => upload.field === field && !upload.line),
1839
2185
  cancelUpload(id) {
@@ -1859,8 +2205,11 @@ function innerForm(options, within, place, outer) {
1859
2205
  // 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
2206
  resets++;
1861
2207
  personal.clear();
2208
+ personalLines.clear();
1862
2209
  filed.clear();
1863
2210
  sentAs = null;
2211
+ // Started again: a new response, timed from now.
2212
+ bot = { ...bot, honeypot: false, since: clock() };
1864
2213
  const values = syncParts((0, values_1.structuredCopy)(baseline), 'start');
1865
2214
  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
2215
  },
@@ -1873,7 +2222,7 @@ function innerForm(options, within, place, outer) {
1873
2222
  const lines = (state.values[field] ?? []).map((line) => line.key === key ? { ...line, values: { ...line.values, [name]: (0, values_1.structuredCopy)(value) } } : line);
1874
2223
  writeValues({ ...state.values, [field]: lines });
1875
2224
  afterEdit(field);
1876
- changed(field, 'person');
2225
+ changed(field, 'person', { line: key });
1877
2226
  },
1878
2227
  moveLine(field, key, to) {
1879
2228
  const def = lineField(field);
@@ -1918,10 +2267,10 @@ function innerForm(options, within, place, outer) {
1918
2267
  const lines = (state.values[field] ?? []).filter((line) => line.key !== key);
1919
2268
  writeValues({ ...state.values, [field]: lines });
1920
2269
  afterEdit(field);
1921
- changed(field, 'person');
2270
+ changed(field, 'person', { line: key });
1922
2271
  },
1923
2272
  async search(field, query, limit = 8, options = {}) {
1924
- return runSearch(fieldDef(field), field, context(), query, limit, options.model, options.ids);
2273
+ return runSearch(fieldDef(field), field, context(), query, limit, options);
1925
2274
  },
1926
2275
  async previewLine(field, key, values) {
1927
2276
  lineField(field);
@@ -1946,7 +2295,7 @@ function innerForm(options, within, place, outer) {
1946
2295
  const line = (state.values[field] ?? []).find((l) => l.key === key);
1947
2296
  if (!line)
1948
2297
  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);
2298
+ return runSearch(sub, subfield, (0, env_1.lineContext)(line.values, def.fields, context(), line.id ?? null), query, limit, options);
1950
2299
  },
1951
2300
  canCreate: (field) => creatable(page.fields[field]) !== null,
1952
2301
  quickCreate: (field, typed) => runCreate(page.fields[field], field, typed, page.fields[field] ? createValuesOf(field) : {}),
@@ -2108,7 +2457,7 @@ function innerForm(options, within, place, outer) {
2108
2457
  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
2458
  });
2110
2459
  },
2111
- run: (list, context = {}) => track(runTop(list, { id: context.id ?? 'run', chain: [], ...(context.recordIds ? { recordIds: [...context.recordIds] } : {}) })),
2460
+ run: (list, context = {}) => track(runTop(list, { id: context.id ?? 'run', chain: [], typed: 'first', ...(context.recordIds ? { recordIds: [...context.recordIds] } : {}) })),
2112
2461
  shown(tab) {
2113
2462
  // A wizard's steps are told by the form itself, as they are entered.
2114
2463
  if (steps.includes(tab))
@@ -2135,20 +2484,36 @@ function innerForm(options, within, place, outer) {
2135
2484
  },
2136
2485
  async reloadTheirs() {
2137
2486
  // A save on its way first: its answer is not dropped by the load.
2487
+ const asked = shownNow();
2138
2488
  await savesSettled();
2139
- // The person's changes, kept before theirs are loaded over them.
2489
+ if (!asked())
2490
+ return false;
2491
+ // What is still typed is the person's too: read, a name looked up waited for, before theirs are loaded.
2492
+ const waiting = readTyped();
2493
+ if (waiting)
2494
+ await waiting;
2495
+ if (!asked())
2496
+ return false;
2497
+ // The person's changes, kept before theirs are loaded over them; and text they left that does not read.
2140
2498
  const mine = {};
2141
2499
  for (const name of state.dirty)
2142
2500
  mine[name] = (0, values_1.structuredCopy)(state.values[name]);
2143
- await track(load(true));
2144
- if (state.status !== 'ready')
2501
+ const typed = state.unread;
2502
+ // The record loaded again is a record shown anew: what is set aside is laid on it alone.
2503
+ const loading = load(true);
2504
+ const loaded = generation;
2505
+ await track(loading);
2506
+ if (loaded !== generation || state.status !== 'ready')
2145
2507
  return false;
2146
2508
  // Set aside where theirs differ from mine: a change they made the same way is no loss.
2147
2509
  const aside = { ...(state.setAside ?? {}) };
2148
2510
  for (const [name, value] of Object.entries(mine))
2149
2511
  if (!sameValue(fields[name], value, state.values[name]))
2150
2512
  aside[name] = value;
2151
- set({ conflict: null, saveProblem: null, setAside: Object.keys(aside).length ? aside : null });
2513
+ // Text that does not read is no value to set aside: it stays in its box, unread with why, over their value —
2514
+ // never dropped unsaid. A line of theirs no longer there takes its cells' text with it.
2515
+ const unread = Object.fromEntries(Object.entries(typed).filter(([path]) => valueAt(state.values, path) !== gone));
2516
+ set({ conflict: null, saveProblem: null, setAside: Object.keys(aside).length ? aside : null, ...(Object.keys(unread).length ? { unread } : {}) });
2152
2517
  return true;
2153
2518
  },
2154
2519
  async keepMine() {
@@ -2159,6 +2524,7 @@ function innerForm(options, within, place, outer) {
2159
2524
  const source = options.dataSource;
2160
2525
  if (conflict && !told) {
2161
2526
  if (source?.load && state.recordId != null && page.data.kind === 'record') {
2527
+ const shown = shownNow();
2162
2528
  try {
2163
2529
  const now = (0, data_source_1.loadedRecordOf)(await track(source.load({ model: page.data.model, id: state.recordId, fields })));
2164
2530
  told = now.values;
@@ -2167,6 +2533,9 @@ function innerForm(options, within, place, outer) {
2167
2533
  catch {
2168
2534
  // Unread: said below.
2169
2535
  }
2536
+ // Read, or not, once another record is shown or the values let go: nothing is laid, nothing saved.
2537
+ if (!shown())
2538
+ return false;
2170
2539
  }
2171
2540
  // Still unread: saving now would go over work this person never saw. Nothing is saved; they are told, and choose again.
2172
2541
  if (!told) {
@@ -2229,7 +2598,8 @@ function innerForm(options, within, place, outer) {
2229
2598
  const given = state.values[part.name];
2230
2599
  // 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
2600
  const made = innerForm({
2232
- page: { ...inner, data: { kind: 'responses' } },
2601
+ // In the record's time zone, unless it names its own.
2602
+ page: { ...inner, data: { kind: 'responses' }, ...(inner.timeZone || !page.timeZone ? {} : { timeZone: page.timeZone }) },
2233
2603
  dataSource: options.dataSource,
2234
2604
  values: isRecord(given) ? given : {},
2235
2605
  onAction: options.onAction,
@@ -2242,7 +2612,7 @@ function innerForm(options, within, place, outer) {
2242
2612
  // Its files are the record's: stored for it, under the part's name.
2243
2613
  { model: where.model, recordId: where.recordId, under: `${where.under}${part.name}.` },
2244
2614
  // Its ways out are the record's: through this form's check.
2245
- { letGo, close, leaveNow: (how) => atOnce(how) });
2615
+ { letGo, close, leaveNow: (how) => atOnce(how), recordShown: shownNow });
2246
2616
  attached.set(id, { name: part.name, inner: made });
2247
2617
  // A change inside it is told as a change of its answers here, whoever made it.
2248
2618
  made.form.on('change', ({ by }) => {
@@ -2486,6 +2856,12 @@ function indexLayout(root) {
2486
2856
  else if (node.type === 'form') {
2487
2857
  add(node, parent, 'form', { invisible: node.invisible, readonly: node.readonly });
2488
2858
  }
2859
+ else if (node.type === 'related') {
2860
+ add(node, parent, 'other', { invisible: node.invisible });
2861
+ // A row's buttons: their roles here, hidden with the list, their condition read by it on each row's record.
2862
+ for (const button of node.rowButtons ?? [])
2863
+ add(button, node.id, 'button', {});
2864
+ }
2489
2865
  else {
2490
2866
  add(node, parent, 'other', { invisible: node.invisible });
2491
2867
  }
@@ -2512,6 +2888,9 @@ function indexLayout(root) {
2512
2888
  // A list holds no fields of its own record: only the buttons for what is selected.
2513
2889
  for (const button of root.actions ?? [])
2514
2890
  add(button, root.id, 'button', { invisible: button.invisible });
2891
+ // A row's buttons: their roles here, their condition read by the list on each row's record, which the form does not hold.
2892
+ for (const button of root.rowButtons ?? [])
2893
+ add(button, root.id, 'button', {});
2515
2894
  }
2516
2895
  else {
2517
2896
  // The title and statusbar show fields too, so they are validated like