@fieldia/core 0.9.2 → 0.11.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 (235) hide show
  1. package/README.md +199 -0
  2. package/package.json +1 -1
  3. package/schema/page.schema.json +1837 -94
  4. package/src/index.d.mts +1 -0
  5. package/src/index.d.ts +1 -0
  6. package/src/index.js +4 -1
  7. package/src/index.js.map +1 -1
  8. package/src/index.mjs +1 -0
  9. package/src/index.mjs.map +1 -1
  10. package/src/lib/expression/evaluator.d.mts +6 -5
  11. package/src/lib/expression/evaluator.d.ts +6 -5
  12. package/src/lib/expression/evaluator.js +37 -22
  13. package/src/lib/expression/evaluator.js.map +1 -1
  14. package/src/lib/expression/evaluator.mjs +32 -18
  15. package/src/lib/expression/evaluator.mjs.map +1 -1
  16. package/src/lib/expression/expression.d.mts +21 -0
  17. package/src/lib/expression/expression.d.ts +21 -0
  18. package/src/lib/expression/expression.js +29 -0
  19. package/src/lib/expression/expression.js.map +1 -0
  20. package/src/lib/expression/expression.mjs +26 -0
  21. package/src/lib/expression/expression.mjs.map +1 -0
  22. package/src/lib/expression/extractFieldNames.js +6 -1
  23. package/src/lib/expression/extractFieldNames.js.map +1 -1
  24. package/src/lib/expression/extractFieldNames.mjs +6 -1
  25. package/src/lib/expression/extractFieldNames.mjs.map +1 -1
  26. package/src/lib/expression/functions.d.mts +35 -0
  27. package/src/lib/expression/functions.d.ts +35 -0
  28. package/src/lib/expression/functions.js +129 -0
  29. package/src/lib/expression/functions.js.map +1 -0
  30. package/src/lib/expression/functions.mjs +124 -0
  31. package/src/lib/expression/functions.mjs.map +1 -0
  32. package/src/lib/expression/modifier.d.mts +3 -1
  33. package/src/lib/expression/modifier.d.ts +3 -1
  34. package/src/lib/expression/modifier.js +4 -27
  35. package/src/lib/expression/modifier.js.map +1 -1
  36. package/src/lib/expression/modifier.mjs +3 -26
  37. package/src/lib/expression/modifier.mjs.map +1 -1
  38. package/src/lib/expression/operations.d.mts +25 -0
  39. package/src/lib/expression/operations.d.ts +25 -0
  40. package/src/lib/expression/operations.js +81 -0
  41. package/src/lib/expression/operations.js.map +1 -0
  42. package/src/lib/expression/operations.mjs +72 -0
  43. package/src/lib/expression/operations.mjs.map +1 -0
  44. package/src/lib/expression/order.d.mts +11 -0
  45. package/src/lib/expression/order.d.ts +11 -0
  46. package/src/lib/expression/order.js +36 -0
  47. package/src/lib/expression/order.js.map +1 -0
  48. package/src/lib/expression/order.mjs +33 -0
  49. package/src/lib/expression/order.mjs.map +1 -0
  50. package/src/lib/expression/parser.d.mts +34 -4
  51. package/src/lib/expression/parser.d.ts +34 -4
  52. package/src/lib/expression/parser.js +72 -6
  53. package/src/lib/expression/parser.js.map +1 -1
  54. package/src/lib/expression/parser.mjs +72 -6
  55. package/src/lib/expression/parser.mjs.map +1 -1
  56. package/src/lib/expression/reads.d.mts +8 -0
  57. package/src/lib/expression/reads.d.ts +8 -0
  58. package/src/lib/expression/reads.js +48 -0
  59. package/src/lib/expression/reads.js.map +1 -0
  60. package/src/lib/expression/reads.mjs +44 -0
  61. package/src/lib/expression/reads.mjs.map +1 -0
  62. package/src/lib/expression/tokenizer.d.mts +7 -6
  63. package/src/lib/expression/tokenizer.d.ts +7 -6
  64. package/src/lib/expression/tokenizer.js +28 -18
  65. package/src/lib/expression/tokenizer.js.map +1 -1
  66. package/src/lib/expression/tokenizer.mjs +28 -18
  67. package/src/lib/expression/tokenizer.mjs.map +1 -1
  68. package/src/lib/format/actions.d.mts +692 -0
  69. package/src/lib/format/actions.d.ts +692 -0
  70. package/src/lib/format/actions.js +53 -0
  71. package/src/lib/format/actions.js.map +1 -0
  72. package/src/lib/format/actions.mjs +50 -0
  73. package/src/lib/format/actions.mjs.map +1 -0
  74. package/src/lib/format/field.d.mts +686 -5
  75. package/src/lib/format/field.d.ts +686 -5
  76. package/src/lib/format/field.js +117 -13
  77. package/src/lib/format/field.js.map +1 -1
  78. package/src/lib/format/field.mjs +116 -12
  79. package/src/lib/format/field.mjs.map +1 -1
  80. package/src/lib/format/layout.d.mts +1130 -22
  81. package/src/lib/format/layout.d.ts +1130 -22
  82. package/src/lib/format/layout.js +67 -5
  83. package/src/lib/format/layout.js.map +1 -1
  84. package/src/lib/format/layout.mjs +66 -4
  85. package/src/lib/format/layout.mjs.map +1 -1
  86. package/src/lib/format/layout.types.d.mts +2 -1
  87. package/src/lib/format/layout.types.d.ts +2 -1
  88. package/src/lib/format/names.d.mts +1 -1
  89. package/src/lib/format/names.d.ts +1 -1
  90. package/src/lib/format/names.js +1 -0
  91. package/src/lib/format/names.js.map +1 -1
  92. package/src/lib/format/names.mjs +1 -0
  93. package/src/lib/format/names.mjs.map +1 -1
  94. package/src/lib/format/page.d.mts +1007 -7
  95. package/src/lib/format/page.d.ts +1007 -7
  96. package/src/lib/format/page.js +28 -0
  97. package/src/lib/format/page.js.map +1 -1
  98. package/src/lib/format/page.mjs +28 -0
  99. package/src/lib/format/page.mjs.map +1 -1
  100. package/src/lib/format/part-look.d.mts +36 -0
  101. package/src/lib/format/part-look.d.ts +36 -0
  102. package/src/lib/format/part-look.js +23 -0
  103. package/src/lib/format/part-look.js.map +1 -0
  104. package/src/lib/format/part-look.mjs +20 -0
  105. package/src/lib/format/part-look.mjs.map +1 -0
  106. package/src/lib/format/references.d.mts +33 -1
  107. package/src/lib/format/references.d.ts +33 -1
  108. package/src/lib/format/references.js +227 -10
  109. package/src/lib/format/references.js.map +1 -1
  110. package/src/lib/format/references.mjs +227 -10
  111. package/src/lib/format/references.mjs.map +1 -1
  112. package/src/lib/format/translate.d.mts +20 -0
  113. package/src/lib/format/translate.d.ts +20 -0
  114. package/src/lib/format/translate.js +79 -21
  115. package/src/lib/format/translate.js.map +1 -1
  116. package/src/lib/format/translate.mjs +76 -21
  117. package/src/lib/format/translate.mjs.map +1 -1
  118. package/src/lib/record/check.d.mts +19 -1
  119. package/src/lib/record/check.d.ts +19 -1
  120. package/src/lib/record/check.js +84 -21
  121. package/src/lib/record/check.js.map +1 -1
  122. package/src/lib/record/check.mjs +82 -21
  123. package/src/lib/record/check.mjs.map +1 -1
  124. package/src/lib/record/compute.d.mts +24 -0
  125. package/src/lib/record/compute.d.ts +24 -0
  126. package/src/lib/record/compute.js +107 -0
  127. package/src/lib/record/compute.js.map +1 -0
  128. package/src/lib/record/compute.mjs +103 -0
  129. package/src/lib/record/compute.mjs.map +1 -0
  130. package/src/lib/record/data-source.d.mts +14 -2
  131. package/src/lib/record/data-source.d.ts +14 -2
  132. package/src/lib/record/data-source.js.map +1 -1
  133. package/src/lib/record/data-source.mjs.map +1 -1
  134. package/src/lib/record/env.d.mts +9 -0
  135. package/src/lib/record/env.d.ts +9 -0
  136. package/src/lib/record/env.js +28 -0
  137. package/src/lib/record/env.js.map +1 -0
  138. package/src/lib/record/env.mjs +25 -0
  139. package/src/lib/record/env.mjs.map +1 -0
  140. package/src/lib/record/form.d.mts +110 -18
  141. package/src/lib/record/form.d.ts +110 -18
  142. package/src/lib/record/form.js +497 -72
  143. package/src/lib/record/form.js.map +1 -1
  144. package/src/lib/record/form.mjs +497 -72
  145. package/src/lib/record/form.mjs.map +1 -1
  146. package/src/lib/record/inputs.d.mts +5 -0
  147. package/src/lib/record/inputs.d.ts +5 -0
  148. package/src/lib/record/inputs.js +47 -0
  149. package/src/lib/record/inputs.js.map +1 -0
  150. package/src/lib/record/inputs.mjs +44 -0
  151. package/src/lib/record/inputs.mjs.map +1 -0
  152. package/src/lib/record/limits.d.mts +13 -0
  153. package/src/lib/record/limits.d.ts +13 -0
  154. package/src/lib/record/limits.js +45 -0
  155. package/src/lib/record/limits.js.map +1 -0
  156. package/src/lib/record/limits.mjs +41 -0
  157. package/src/lib/record/limits.mjs.map +1 -0
  158. package/src/lib/record/locales/all.d.mts +7 -0
  159. package/src/lib/record/locales/all.d.ts +7 -0
  160. package/src/lib/record/locales/all.js +13 -0
  161. package/src/lib/record/locales/all.js.map +1 -0
  162. package/src/lib/record/locales/all.mjs +10 -0
  163. package/src/lib/record/locales/all.mjs.map +1 -0
  164. package/src/lib/record/locales/ar.d.mts +3 -0
  165. package/src/lib/record/locales/ar.d.ts +3 -0
  166. package/src/lib/record/locales/ar.js +55 -0
  167. package/src/lib/record/locales/ar.js.map +1 -0
  168. package/src/lib/record/locales/ar.mjs +52 -0
  169. package/src/lib/record/locales/ar.mjs.map +1 -0
  170. package/src/lib/record/locales/de.d.mts +3 -0
  171. package/src/lib/record/locales/de.d.ts +3 -0
  172. package/src/lib/record/locales/de.js +55 -0
  173. package/src/lib/record/locales/de.js.map +1 -0
  174. package/src/lib/record/locales/de.mjs +52 -0
  175. package/src/lib/record/locales/de.mjs.map +1 -0
  176. package/src/lib/record/locales/fr.d.mts +3 -0
  177. package/src/lib/record/locales/fr.d.ts +3 -0
  178. package/src/lib/record/locales/fr.js +55 -0
  179. package/src/lib/record/locales/fr.js.map +1 -0
  180. package/src/lib/record/locales/fr.mjs +52 -0
  181. package/src/lib/record/locales/fr.mjs.map +1 -0
  182. package/src/lib/record/memory-data-source.d.mts +3 -0
  183. package/src/lib/record/memory-data-source.d.ts +3 -0
  184. package/src/lib/record/memory-data-source.js +8 -0
  185. package/src/lib/record/memory-data-source.js.map +1 -1
  186. package/src/lib/record/memory-data-source.mjs +8 -0
  187. package/src/lib/record/memory-data-source.mjs.map +1 -1
  188. package/src/lib/record/messages.d.mts +46 -0
  189. package/src/lib/record/messages.d.ts +46 -0
  190. package/src/lib/record/messages.js +58 -101
  191. package/src/lib/record/messages.js.map +1 -1
  192. package/src/lib/record/messages.mjs +58 -101
  193. package/src/lib/record/messages.mjs.map +1 -1
  194. package/src/lib/record/rules.d.mts +33 -0
  195. package/src/lib/record/rules.d.ts +33 -0
  196. package/src/lib/record/rules.js +83 -0
  197. package/src/lib/record/rules.js.map +1 -0
  198. package/src/lib/record/rules.mjs +79 -0
  199. package/src/lib/record/rules.mjs.map +1 -0
  200. package/src/lib/record/run.d.mts +169 -0
  201. package/src/lib/record/run.d.ts +169 -0
  202. package/src/lib/record/run.js +350 -0
  203. package/src/lib/record/run.js.map +1 -0
  204. package/src/lib/record/run.mjs +345 -0
  205. package/src/lib/record/run.mjs.map +1 -0
  206. package/src/lib/record/score.d.mts +16 -0
  207. package/src/lib/record/score.d.ts +16 -0
  208. package/src/lib/record/score.js +35 -0
  209. package/src/lib/record/score.js.map +1 -0
  210. package/src/lib/record/score.mjs +32 -0
  211. package/src/lib/record/score.mjs.map +1 -0
  212. package/src/lib/record/set-when.d.mts +24 -0
  213. package/src/lib/record/set-when.d.ts +24 -0
  214. package/src/lib/record/set-when.js +68 -0
  215. package/src/lib/record/set-when.js.map +1 -0
  216. package/src/lib/record/set-when.mjs +65 -0
  217. package/src/lib/record/set-when.mjs.map +1 -0
  218. package/src/lib/record/structures.d.mts +14 -0
  219. package/src/lib/record/structures.d.ts +14 -0
  220. package/src/lib/record/structures.js +32 -0
  221. package/src/lib/record/structures.js.map +1 -0
  222. package/src/lib/record/structures.mjs +28 -0
  223. package/src/lib/record/structures.mjs.map +1 -0
  224. package/src/lib/record/values.d.mts +10 -5
  225. package/src/lib/record/values.d.ts +10 -5
  226. package/src/lib/record/values.js +20 -6
  227. package/src/lib/record/values.js.map +1 -1
  228. package/src/lib/record/values.mjs +19 -6
  229. package/src/lib/record/values.mjs.map +1 -1
  230. package/src/runtime.d.mts +13 -7
  231. package/src/runtime.d.ts +13 -7
  232. package/src/runtime.js +14 -1
  233. package/src/runtime.js.map +1 -1
  234. package/src/runtime.mjs +6 -2
  235. package/src/runtime.mjs.map +1 -1
@@ -1,18 +1,25 @@
1
- import type { JsonValue } from '../format/json.mjs';
1
+ import type { ActionStep } from '../format/actions.mjs';
2
+ import type { Option } from '../format/field.mjs';
2
3
  import type { Page } from '../format/page.mjs';
3
4
  import { type Messages } from './messages.mjs';
4
5
  import { type DataSource, type RecordChanges, type SaveProblem } from './data-source.mjs';
5
6
  import { type Scheduler } from './scheduler.mjs';
7
+ import { type ActionHost, type ActionRequest, type ActionResult, type OnAction, type RunContext, type RunResult } from './run.mjs';
6
8
  import { type RecordId, type RelatedRecord, type Value, type Values } from './values.mjs';
7
9
  export type FormStatus = 'idle' | 'loading' | 'ready' | 'saving' | 'saved' | 'error';
8
10
  export interface FormState {
9
11
  readonly status: FormStatus;
10
12
  readonly recordId: RecordId | null;
11
13
  readonly values: Readonly<Values>;
12
- /** Messages by field name; a line's field is `field.lineKey.subfield`. */
14
+ /** Messages by field name; a line's field is `field.lineKey.subfield`, a saved form's field `name.field`. */
13
15
  readonly errors: Readonly<Record<string, string>>;
14
16
  /** Fields that differ from the record as loaded or last saved. */
15
17
  readonly dirty: readonly string[];
18
+ /**
19
+ * Warnings by field name, from answer rules at the warning level: shown,
20
+ * never blocking. Kept current as values change, for the fields people can see.
21
+ */
22
+ readonly warnings: Readonly<Record<string, string>>;
16
23
  /** A warning from the data source's onchange, shown without blocking. */
17
24
  readonly warning: string | null;
18
25
  /** The field whose change brought the warning, so it can be shown beside it. */
@@ -29,6 +36,16 @@ export interface FormState {
29
36
  readonly skipped: readonly string[];
30
37
  /** Why the last save was refused, until the next one starts or the form is reset. */
31
38
  readonly saveProblem: SaveProblem | null;
39
+ /** The choices loaded from the app's lists, by field: those of a selection with `optionsFrom`, once asked for. */
40
+ readonly choices: Readonly<Record<string, Choices>>;
41
+ }
42
+ /** A field's choices from one of the app's lists, as they load. */
43
+ export interface Choices {
44
+ /** The choices last loaded: null until the first load ends well. */
45
+ readonly options: Option[] | null;
46
+ readonly loading?: boolean;
47
+ /** Why the last load failed. */
48
+ readonly error?: string;
32
49
  }
33
50
  /** What a layout element looks like right now. */
34
51
  export interface NodeState {
@@ -42,17 +59,47 @@ export interface DraftStore {
42
59
  setItem(key: string, value: string): void;
43
60
  removeItem(key: string): void;
44
61
  }
45
- export interface ActionRequest {
46
- /** The button or stat button pressed. */
47
- id: string;
48
- action: string;
49
- params?: {
50
- [key: string]: JsonValue;
62
+ export type { ActionRequest } from './run.mjs';
63
+ /** Whose change it was: a person's (in the UI), a step's, or the app's (`setValues`, or `setValue` told so). Only a person's runs the page's `change` steps. */
64
+ export type ChangeBy = 'person' | 'step' | 'app';
65
+ /** What a form tells the app, by event: `form.on(event, listener)`. */
66
+ export interface FormEvents {
67
+ /** A field was written: by a person, a step or the app. Values worked out from it are not told. */
68
+ change: {
69
+ field: string;
70
+ value: Value;
71
+ values: Readonly<Values>;
72
+ by: ChangeBy;
73
+ };
74
+ /** A record was saved. */
75
+ save: {
76
+ recordId: RecordId | null;
77
+ values: Readonly<Values>;
78
+ };
79
+ /** A page of responses sent its answers: those sent. */
80
+ send: {
81
+ values: Values;
82
+ };
83
+ /** The app answered a call: with nothing (undefined), or an `ActionResult`. */
84
+ action: {
85
+ request: ActionRequest;
86
+ result: ActionResult | undefined;
87
+ };
88
+ /** A run of steps ended — a button's, a moment's, `form.run`'s — and how. */
89
+ run: {
90
+ id: string;
91
+ steps: readonly ActionStep[];
92
+ result: RunResult;
93
+ };
94
+ /** A wizard's step was entered. */
95
+ step: {
96
+ step: string;
97
+ };
98
+ /** The form has its values: loaded, or new. */
99
+ open: {
100
+ recordId: RecordId | null;
101
+ values: Readonly<Values>;
51
102
  };
52
- recordId: RecordId | null;
53
- values: Values;
54
- /** The records chosen in a list, when the button belongs to one. */
55
- recordIds?: RecordId[];
56
103
  }
57
104
  export interface FormOptions {
58
105
  page: Page;
@@ -61,8 +108,17 @@ export interface FormOptions {
61
108
  recordId?: RecordId | null;
62
109
  /** Values to start with, on top of each field's default. */
63
110
  values?: Values;
64
- /** What a button does. Fieldia hands the press over; the app decides. */
65
- onAction?: (request: ActionRequest) => void | Promise<void>;
111
+ /**
112
+ * The app's own actions: a button's `action`, and each `call` step. Fieldia
113
+ * hands the request over and waits; the app may answer (`ActionResult`)
114
+ * with values to set, words to say, a page to open, or to stop. Nothing
115
+ * answered means go on.
116
+ */
117
+ onAction?: OnAction;
118
+ /** The UI the core cannot draw — pages opened, words said, questions asked — given by the viewer. */
119
+ host?: ActionHost;
120
+ /** How a question is asked when there is no host. Without either, a question goes on as though answered Yes. */
121
+ confirm?: (message: string) => boolean | Promise<boolean>;
66
122
  drafts?: {
67
123
  store: DraftStore;
68
124
  restore?: 'auto' | 'ask';
@@ -74,13 +130,20 @@ export interface FormOptions {
74
130
  scheduler?: Scheduler;
75
131
  /** Validation messages in the page's language. English by default. */
76
132
  messages?: Messages;
133
+ /** The clock, for `today()` and for dates that must be past or future. The computer's own when left out. */
134
+ now?: () => Date;
77
135
  }
78
136
  export interface Form {
79
137
  readonly page: Page;
80
138
  getState(): FormState;
81
139
  subscribe(listener: (state: FormState) => void): () => void;
82
140
  load(): Promise<void>;
83
- setValue(name: string, value: Value): void;
141
+ /** Write a field: a person's change unless told otherwise, and only a person's runs the page's `change` steps. */
142
+ setValue(name: string, value: Value, how?: {
143
+ by?: ChangeBy;
144
+ }): void;
145
+ /** Write several fields at once, worked out once: the app's change, never a person's. Throws for a field the page lacks. */
146
+ setValues(values: Values): void;
84
147
  node(id: string): NodeState;
85
148
  fieldReadonly(name: string): boolean;
86
149
  /** What a field would be told if the form were checked now, or null when it passes. Nothing is shown. */
@@ -100,6 +163,12 @@ export interface Form {
100
163
  * written: the record, its changes and its autosave stay as they are.
101
164
  */
102
165
  previewLine(field: string, key: string, values: Values): Promise<Values>;
166
+ /**
167
+ * Load, or load again, the choices of a selection whose options come from
168
+ * the app's list (`optionsFrom`). Once loaded, they load again by themselves
169
+ * when a field they change with changes; the latest answer wins.
170
+ */
171
+ loadChoices(field: string): void;
103
172
  /** Records a many2one, many2many or reference may point to. A reference needs `options.model`. */
104
173
  search(field: string, query: string, limit?: number, options?: {
105
174
  model?: string;
@@ -124,14 +193,37 @@ export interface Form {
124
193
  * an optional one instead.
125
194
  */
126
195
  goTo(step: string): boolean;
127
- /** Run a button; a list's button passes the records chosen in it. */
196
+ /**
197
+ * Press a button: ask its `confirm` first, run its `steps`, then call its
198
+ * `action`, when it names one, as a final `call`. A list's button passes
199
+ * the records chosen in it, and every call of the run carries them.
200
+ */
128
201
  runAction(id: string, chosen?: {
129
202
  recordIds: RecordId[];
130
- }): Promise<void>;
203
+ }): Promise<RunResult>;
204
+ /** Run steps on this form, as a button would: `context.id` is what their calls carry (`run` when left out). */
205
+ run(steps: readonly ActionStep[], context?: RunContext): Promise<RunResult>;
206
+ /** A tab was shown: the viewer says so, and the page's `show` steps for it run. */
207
+ shown(tab: string): void;
208
+ /** Listen to one of the form's events; returns what stops listening. */
209
+ on<E extends keyof FormEvents>(event: E, listener: (event: FormEvents[E]) => void): () => void;
131
210
  restoreDraft(): void;
132
211
  discardDraft(): void;
133
- /** Resolves once every onchange, save and autosave started so far has finished. */
212
+ /** Resolves once every onchange, save, autosave and run of steps started so far has finished — a question still waiting for its answer included. */
134
213
  settled(): Promise<void>;
214
+ /**
215
+ * Give a saved form placed on this page (a `form` part, by its id) its page,
216
+ * once the app has found it. From then its answers sit under the part's
217
+ * name as an object, checked by that page's own rules: this form refuses
218
+ * to save or send while one of them fails, with the error under
219
+ * `name.field`, and shows it on the inner form beside its field. Returns the
220
+ * inner form, which a view draws the part's fields from; giving the page
221
+ * again returns the same one. A page placed inside itself, directly or
222
+ * through others, is refused.
223
+ */
224
+ embed(id: string, page: Page): Form;
225
+ /** The inner form of a saved form placed on this page, once it has its page; null until then. */
226
+ embedded(id: string): Form | null;
135
227
  dispose(): void;
136
228
  }
137
229
  /**
@@ -1,18 +1,25 @@
1
- import type { JsonValue } from '../format/json';
1
+ import type { ActionStep } from '../format/actions';
2
+ import type { Option } from '../format/field';
2
3
  import type { Page } from '../format/page';
3
4
  import { type Messages } from './messages';
4
5
  import { type DataSource, type RecordChanges, type SaveProblem } from './data-source';
5
6
  import { type Scheduler } from './scheduler';
7
+ import { type ActionHost, type ActionRequest, type ActionResult, type OnAction, type RunContext, type RunResult } from './run';
6
8
  import { type RecordId, type RelatedRecord, type Value, type Values } from './values';
7
9
  export type FormStatus = 'idle' | 'loading' | 'ready' | 'saving' | 'saved' | 'error';
8
10
  export interface FormState {
9
11
  readonly status: FormStatus;
10
12
  readonly recordId: RecordId | null;
11
13
  readonly values: Readonly<Values>;
12
- /** Messages by field name; a line's field is `field.lineKey.subfield`. */
14
+ /** Messages by field name; a line's field is `field.lineKey.subfield`, a saved form's field `name.field`. */
13
15
  readonly errors: Readonly<Record<string, string>>;
14
16
  /** Fields that differ from the record as loaded or last saved. */
15
17
  readonly dirty: readonly string[];
18
+ /**
19
+ * Warnings by field name, from answer rules at the warning level: shown,
20
+ * never blocking. Kept current as values change, for the fields people can see.
21
+ */
22
+ readonly warnings: Readonly<Record<string, string>>;
16
23
  /** A warning from the data source's onchange, shown without blocking. */
17
24
  readonly warning: string | null;
18
25
  /** The field whose change brought the warning, so it can be shown beside it. */
@@ -29,6 +36,16 @@ export interface FormState {
29
36
  readonly skipped: readonly string[];
30
37
  /** Why the last save was refused, until the next one starts or the form is reset. */
31
38
  readonly saveProblem: SaveProblem | null;
39
+ /** The choices loaded from the app's lists, by field: those of a selection with `optionsFrom`, once asked for. */
40
+ readonly choices: Readonly<Record<string, Choices>>;
41
+ }
42
+ /** A field's choices from one of the app's lists, as they load. */
43
+ export interface Choices {
44
+ /** The choices last loaded: null until the first load ends well. */
45
+ readonly options: Option[] | null;
46
+ readonly loading?: boolean;
47
+ /** Why the last load failed. */
48
+ readonly error?: string;
32
49
  }
33
50
  /** What a layout element looks like right now. */
34
51
  export interface NodeState {
@@ -42,17 +59,47 @@ export interface DraftStore {
42
59
  setItem(key: string, value: string): void;
43
60
  removeItem(key: string): void;
44
61
  }
45
- export interface ActionRequest {
46
- /** The button or stat button pressed. */
47
- id: string;
48
- action: string;
49
- params?: {
50
- [key: string]: JsonValue;
62
+ export type { ActionRequest } from './run';
63
+ /** Whose change it was: a person's (in the UI), a step's, or the app's (`setValues`, or `setValue` told so). Only a person's runs the page's `change` steps. */
64
+ export type ChangeBy = 'person' | 'step' | 'app';
65
+ /** What a form tells the app, by event: `form.on(event, listener)`. */
66
+ export interface FormEvents {
67
+ /** A field was written: by a person, a step or the app. Values worked out from it are not told. */
68
+ change: {
69
+ field: string;
70
+ value: Value;
71
+ values: Readonly<Values>;
72
+ by: ChangeBy;
73
+ };
74
+ /** A record was saved. */
75
+ save: {
76
+ recordId: RecordId | null;
77
+ values: Readonly<Values>;
78
+ };
79
+ /** A page of responses sent its answers: those sent. */
80
+ send: {
81
+ values: Values;
82
+ };
83
+ /** The app answered a call: with nothing (undefined), or an `ActionResult`. */
84
+ action: {
85
+ request: ActionRequest;
86
+ result: ActionResult | undefined;
87
+ };
88
+ /** A run of steps ended — a button's, a moment's, `form.run`'s — and how. */
89
+ run: {
90
+ id: string;
91
+ steps: readonly ActionStep[];
92
+ result: RunResult;
93
+ };
94
+ /** A wizard's step was entered. */
95
+ step: {
96
+ step: string;
97
+ };
98
+ /** The form has its values: loaded, or new. */
99
+ open: {
100
+ recordId: RecordId | null;
101
+ values: Readonly<Values>;
51
102
  };
52
- recordId: RecordId | null;
53
- values: Values;
54
- /** The records chosen in a list, when the button belongs to one. */
55
- recordIds?: RecordId[];
56
103
  }
57
104
  export interface FormOptions {
58
105
  page: Page;
@@ -61,8 +108,17 @@ export interface FormOptions {
61
108
  recordId?: RecordId | null;
62
109
  /** Values to start with, on top of each field's default. */
63
110
  values?: Values;
64
- /** What a button does. Fieldia hands the press over; the app decides. */
65
- onAction?: (request: ActionRequest) => void | Promise<void>;
111
+ /**
112
+ * The app's own actions: a button's `action`, and each `call` step. Fieldia
113
+ * hands the request over and waits; the app may answer (`ActionResult`)
114
+ * with values to set, words to say, a page to open, or to stop. Nothing
115
+ * answered means go on.
116
+ */
117
+ onAction?: OnAction;
118
+ /** The UI the core cannot draw — pages opened, words said, questions asked — given by the viewer. */
119
+ host?: ActionHost;
120
+ /** How a question is asked when there is no host. Without either, a question goes on as though answered Yes. */
121
+ confirm?: (message: string) => boolean | Promise<boolean>;
66
122
  drafts?: {
67
123
  store: DraftStore;
68
124
  restore?: 'auto' | 'ask';
@@ -74,13 +130,20 @@ export interface FormOptions {
74
130
  scheduler?: Scheduler;
75
131
  /** Validation messages in the page's language. English by default. */
76
132
  messages?: Messages;
133
+ /** The clock, for `today()` and for dates that must be past or future. The computer's own when left out. */
134
+ now?: () => Date;
77
135
  }
78
136
  export interface Form {
79
137
  readonly page: Page;
80
138
  getState(): FormState;
81
139
  subscribe(listener: (state: FormState) => void): () => void;
82
140
  load(): Promise<void>;
83
- setValue(name: string, value: Value): void;
141
+ /** Write a field: a person's change unless told otherwise, and only a person's runs the page's `change` steps. */
142
+ setValue(name: string, value: Value, how?: {
143
+ by?: ChangeBy;
144
+ }): void;
145
+ /** Write several fields at once, worked out once: the app's change, never a person's. Throws for a field the page lacks. */
146
+ setValues(values: Values): void;
84
147
  node(id: string): NodeState;
85
148
  fieldReadonly(name: string): boolean;
86
149
  /** What a field would be told if the form were checked now, or null when it passes. Nothing is shown. */
@@ -100,6 +163,12 @@ export interface Form {
100
163
  * written: the record, its changes and its autosave stay as they are.
101
164
  */
102
165
  previewLine(field: string, key: string, values: Values): Promise<Values>;
166
+ /**
167
+ * Load, or load again, the choices of a selection whose options come from
168
+ * the app's list (`optionsFrom`). Once loaded, they load again by themselves
169
+ * when a field they change with changes; the latest answer wins.
170
+ */
171
+ loadChoices(field: string): void;
103
172
  /** Records a many2one, many2many or reference may point to. A reference needs `options.model`. */
104
173
  search(field: string, query: string, limit?: number, options?: {
105
174
  model?: string;
@@ -124,14 +193,37 @@ export interface Form {
124
193
  * an optional one instead.
125
194
  */
126
195
  goTo(step: string): boolean;
127
- /** Run a button; a list's button passes the records chosen in it. */
196
+ /**
197
+ * Press a button: ask its `confirm` first, run its `steps`, then call its
198
+ * `action`, when it names one, as a final `call`. A list's button passes
199
+ * the records chosen in it, and every call of the run carries them.
200
+ */
128
201
  runAction(id: string, chosen?: {
129
202
  recordIds: RecordId[];
130
- }): Promise<void>;
203
+ }): Promise<RunResult>;
204
+ /** Run steps on this form, as a button would: `context.id` is what their calls carry (`run` when left out). */
205
+ run(steps: readonly ActionStep[], context?: RunContext): Promise<RunResult>;
206
+ /** A tab was shown: the viewer says so, and the page's `show` steps for it run. */
207
+ shown(tab: string): void;
208
+ /** Listen to one of the form's events; returns what stops listening. */
209
+ on<E extends keyof FormEvents>(event: E, listener: (event: FormEvents[E]) => void): () => void;
131
210
  restoreDraft(): void;
132
211
  discardDraft(): void;
133
- /** Resolves once every onchange, save and autosave started so far has finished. */
212
+ /** Resolves once every onchange, save, autosave and run of steps started so far has finished — a question still waiting for its answer included. */
134
213
  settled(): Promise<void>;
214
+ /**
215
+ * Give a saved form placed on this page (a `form` part, by its id) its page,
216
+ * once the app has found it. From then its answers sit under the part's
217
+ * name as an object, checked by that page's own rules: this form refuses
218
+ * to save or send while one of them fails, with the error under
219
+ * `name.field`, and shows it on the inner form beside its field. Returns the
220
+ * inner form, which a view draws the part's fields from; giving the page
221
+ * again returns the same one. A page placed inside itself, directly or
222
+ * through others, is refused.
223
+ */
224
+ embed(id: string, page: Page): Form;
225
+ /** The inner form of a saved form placed on this page, once it has its page; null until then. */
226
+ embedded(id: string): Form | null;
135
227
  dispose(): void;
136
228
  }
137
229
  /**