@fieldia/core 0.11.0 → 0.13.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 (203) hide show
  1. package/README.md +5 -0
  2. package/package.json +1 -1
  3. package/schema/page.schema.json +3998 -893
  4. package/src/index.d.mts +1 -1
  5. package/src/index.d.ts +1 -1
  6. package/src/index.js +4 -1
  7. package/src/index.js.map +1 -1
  8. package/src/index.mjs +1 -1
  9. package/src/index.mjs.map +1 -1
  10. package/src/lib/expression/built-ins.d.mts +4 -0
  11. package/src/lib/expression/built-ins.d.ts +4 -0
  12. package/src/lib/expression/built-ins.js +8 -0
  13. package/src/lib/expression/built-ins.js.map +1 -0
  14. package/src/lib/expression/built-ins.mjs +5 -0
  15. package/src/lib/expression/built-ins.mjs.map +1 -0
  16. package/src/lib/expression/evaluator.js +17 -7
  17. package/src/lib/expression/evaluator.js.map +1 -1
  18. package/src/lib/expression/evaluator.mjs +17 -7
  19. package/src/lib/expression/evaluator.mjs.map +1 -1
  20. package/src/lib/expression/expression.d.mts +7 -0
  21. package/src/lib/expression/expression.d.ts +7 -0
  22. package/src/lib/expression/expression.js +2 -0
  23. package/src/lib/expression/expression.js.map +1 -1
  24. package/src/lib/expression/expression.mjs +3 -1
  25. package/src/lib/expression/expression.mjs.map +1 -1
  26. package/src/lib/expression/functions.d.mts +2 -0
  27. package/src/lib/expression/functions.d.ts +2 -0
  28. package/src/lib/expression/functions.js +38 -5
  29. package/src/lib/expression/functions.js.map +1 -1
  30. package/src/lib/expression/functions.mjs +38 -5
  31. package/src/lib/expression/functions.mjs.map +1 -1
  32. package/src/lib/expression/modifier.d.mts +7 -0
  33. package/src/lib/expression/modifier.d.ts +7 -0
  34. package/src/lib/expression/modifier.js +3 -1
  35. package/src/lib/expression/modifier.js.map +1 -1
  36. package/src/lib/expression/modifier.mjs +4 -2
  37. package/src/lib/expression/modifier.mjs.map +1 -1
  38. package/src/lib/expression/parser.d.mts +8 -1
  39. package/src/lib/expression/parser.d.ts +8 -1
  40. package/src/lib/expression/parser.js +30 -2
  41. package/src/lib/expression/parser.js.map +1 -1
  42. package/src/lib/expression/parser.mjs +30 -2
  43. package/src/lib/expression/parser.mjs.map +1 -1
  44. package/src/lib/expression/reads.d.mts +7 -0
  45. package/src/lib/expression/reads.d.ts +7 -0
  46. package/src/lib/expression/reads.js +23 -0
  47. package/src/lib/expression/reads.js.map +1 -1
  48. package/src/lib/expression/reads.mjs +21 -0
  49. package/src/lib/expression/reads.mjs.map +1 -1
  50. package/src/lib/format/actions.d.mts +486 -2
  51. package/src/lib/format/actions.d.ts +486 -2
  52. package/src/lib/format/actions.js +9 -0
  53. package/src/lib/format/actions.js.map +1 -1
  54. package/src/lib/format/actions.mjs +9 -0
  55. package/src/lib/format/actions.mjs.map +1 -1
  56. package/src/lib/format/check-page.d.mts +7 -2
  57. package/src/lib/format/check-page.d.ts +7 -2
  58. package/src/lib/format/check-page.js +6 -3
  59. package/src/lib/format/check-page.js.map +1 -1
  60. package/src/lib/format/check-page.mjs +6 -3
  61. package/src/lib/format/check-page.mjs.map +1 -1
  62. package/src/lib/format/field.d.mts +3285 -214
  63. package/src/lib/format/field.d.ts +3285 -214
  64. package/src/lib/format/field.js +101 -14
  65. package/src/lib/format/field.js.map +1 -1
  66. package/src/lib/format/field.mjs +100 -13
  67. package/src/lib/format/field.mjs.map +1 -1
  68. package/src/lib/format/hotkeys.d.mts +6 -0
  69. package/src/lib/format/hotkeys.d.ts +6 -0
  70. package/src/lib/format/hotkeys.js +11 -0
  71. package/src/lib/format/hotkeys.js.map +1 -0
  72. package/src/lib/format/hotkeys.mjs +7 -0
  73. package/src/lib/format/hotkeys.mjs.map +1 -0
  74. package/src/lib/format/layout.d.mts +4185 -352
  75. package/src/lib/format/layout.d.ts +4185 -352
  76. package/src/lib/format/layout.js +131 -20
  77. package/src/lib/format/layout.js.map +1 -1
  78. package/src/lib/format/layout.mjs +130 -19
  79. package/src/lib/format/layout.mjs.map +1 -1
  80. package/src/lib/format/page.d.mts +2938 -196
  81. package/src/lib/format/page.d.ts +2938 -196
  82. package/src/lib/format/page.js +7 -1
  83. package/src/lib/format/page.js.map +1 -1
  84. package/src/lib/format/page.mjs +6 -1
  85. package/src/lib/format/page.mjs.map +1 -1
  86. package/src/lib/format/references.d.mts +35 -2
  87. package/src/lib/format/references.d.ts +35 -2
  88. package/src/lib/format/references.js +321 -43
  89. package/src/lib/format/references.js.map +1 -1
  90. package/src/lib/format/references.mjs +321 -43
  91. package/src/lib/format/references.mjs.map +1 -1
  92. package/src/lib/format/themes.d.mts +9 -0
  93. package/src/lib/format/themes.d.ts +9 -0
  94. package/src/lib/format/themes.js +13 -0
  95. package/src/lib/format/themes.js.map +1 -0
  96. package/src/lib/format/themes.mjs +10 -0
  97. package/src/lib/format/themes.mjs.map +1 -0
  98. package/src/lib/format/translate.js +5 -3
  99. package/src/lib/format/translate.js.map +1 -1
  100. package/src/lib/format/translate.mjs +5 -3
  101. package/src/lib/format/translate.mjs.map +1 -1
  102. package/src/lib/format/words.d.mts +9 -0
  103. package/src/lib/format/words.d.ts +9 -0
  104. package/src/lib/format/words.js +35 -0
  105. package/src/lib/format/words.js.map +1 -0
  106. package/src/lib/format/words.mjs +30 -0
  107. package/src/lib/format/words.mjs.map +1 -0
  108. package/src/lib/record/cells.d.mts +37 -0
  109. package/src/lib/record/cells.d.ts +37 -0
  110. package/src/lib/record/cells.js +59 -0
  111. package/src/lib/record/cells.js.map +1 -0
  112. package/src/lib/record/cells.mjs +54 -0
  113. package/src/lib/record/cells.mjs.map +1 -0
  114. package/src/lib/record/compute.d.mts +5 -4
  115. package/src/lib/record/compute.d.ts +5 -4
  116. package/src/lib/record/compute.js +11 -10
  117. package/src/lib/record/compute.js.map +1 -1
  118. package/src/lib/record/compute.mjs +12 -11
  119. package/src/lib/record/compute.mjs.map +1 -1
  120. package/src/lib/record/data-source.d.mts +32 -2
  121. package/src/lib/record/data-source.d.ts +32 -2
  122. package/src/lib/record/data-source.js.map +1 -1
  123. package/src/lib/record/data-source.mjs.map +1 -1
  124. package/src/lib/record/env.d.mts +38 -1
  125. package/src/lib/record/env.d.ts +38 -1
  126. package/src/lib/record/env.js +41 -3
  127. package/src/lib/record/env.js.map +1 -1
  128. package/src/lib/record/env.mjs +38 -3
  129. package/src/lib/record/env.mjs.map +1 -1
  130. package/src/lib/record/filter.d.mts +10 -0
  131. package/src/lib/record/filter.d.ts +10 -0
  132. package/src/lib/record/filter.js +49 -0
  133. package/src/lib/record/filter.js.map +1 -1
  134. package/src/lib/record/filter.mjs +48 -0
  135. package/src/lib/record/filter.mjs.map +1 -1
  136. package/src/lib/record/form.d.mts +143 -8
  137. package/src/lib/record/form.d.ts +143 -8
  138. package/src/lib/record/form.js +495 -58
  139. package/src/lib/record/form.js.map +1 -1
  140. package/src/lib/record/form.mjs +496 -60
  141. package/src/lib/record/form.mjs.map +1 -1
  142. package/src/lib/record/locales/ar.js +3 -0
  143. package/src/lib/record/locales/ar.js.map +1 -1
  144. package/src/lib/record/locales/ar.mjs +3 -0
  145. package/src/lib/record/locales/ar.mjs.map +1 -1
  146. package/src/lib/record/locales/de.js +3 -0
  147. package/src/lib/record/locales/de.js.map +1 -1
  148. package/src/lib/record/locales/de.mjs +3 -0
  149. package/src/lib/record/locales/de.mjs.map +1 -1
  150. package/src/lib/record/locales/fr.js +3 -0
  151. package/src/lib/record/locales/fr.js.map +1 -1
  152. package/src/lib/record/locales/fr.mjs +3 -0
  153. package/src/lib/record/locales/fr.mjs.map +1 -1
  154. package/src/lib/record/memory-data-source.d.mts +26 -2
  155. package/src/lib/record/memory-data-source.d.ts +26 -2
  156. package/src/lib/record/memory-data-source.js +98 -7
  157. package/src/lib/record/memory-data-source.js.map +1 -1
  158. package/src/lib/record/memory-data-source.mjs +98 -7
  159. package/src/lib/record/memory-data-source.mjs.map +1 -1
  160. package/src/lib/record/messages.d.mts +5 -0
  161. package/src/lib/record/messages.d.ts +5 -0
  162. package/src/lib/record/messages.js +3 -0
  163. package/src/lib/record/messages.js.map +1 -1
  164. package/src/lib/record/messages.mjs +3 -0
  165. package/src/lib/record/messages.mjs.map +1 -1
  166. package/src/lib/record/roles.d.mts +10 -0
  167. package/src/lib/record/roles.d.ts +10 -0
  168. package/src/lib/record/roles.js +21 -0
  169. package/src/lib/record/roles.js.map +1 -0
  170. package/src/lib/record/roles.mjs +18 -0
  171. package/src/lib/record/roles.mjs.map +1 -0
  172. package/src/lib/record/rules.js +30 -0
  173. package/src/lib/record/rules.js.map +1 -1
  174. package/src/lib/record/rules.mjs +31 -1
  175. package/src/lib/record/rules.mjs.map +1 -1
  176. package/src/lib/record/run.d.mts +79 -0
  177. package/src/lib/record/run.d.ts +79 -0
  178. package/src/lib/record/run.js +105 -0
  179. package/src/lib/record/run.js.map +1 -1
  180. package/src/lib/record/run.mjs +105 -0
  181. package/src/lib/record/run.mjs.map +1 -1
  182. package/src/lib/record/set-when.d.mts +5 -2
  183. package/src/lib/record/set-when.d.ts +5 -2
  184. package/src/lib/record/set-when.js +28 -12
  185. package/src/lib/record/set-when.js.map +1 -1
  186. package/src/lib/record/set-when.mjs +30 -14
  187. package/src/lib/record/set-when.mjs.map +1 -1
  188. package/src/lib/record/value-map.d.mts +41 -0
  189. package/src/lib/record/value-map.d.ts +41 -0
  190. package/src/lib/record/value-map.js +80 -0
  191. package/src/lib/record/value-map.js.map +1 -0
  192. package/src/lib/record/value-map.mjs +73 -0
  193. package/src/lib/record/value-map.mjs.map +1 -0
  194. package/src/lib/record/values.d.mts +10 -0
  195. package/src/lib/record/values.d.ts +10 -0
  196. package/src/lib/record/values.js.map +1 -1
  197. package/src/lib/record/values.mjs.map +1 -1
  198. package/src/runtime.d.mts +11 -6
  199. package/src/runtime.d.ts +11 -6
  200. package/src/runtime.js +13 -1
  201. package/src/runtime.js.map +1 -1
  202. package/src/runtime.mjs +4 -0
  203. package/src/runtime.mjs.map +1 -1
@@ -1,6 +1,9 @@
1
+ import { BUILT_IN_NAMES, USER_PARTS } from '../expression/built-ins.mjs';
1
2
  import { compileModifier } from '../expression/modifier.mjs';
2
3
  import { compileExpression } from '../expression/expression.mjs';
3
4
  import { dependencyOrder } from '../expression/order.mjs';
5
+ import { valuesIn } from './words.mjs';
6
+ import { BROWSER_HOTKEYS, hotkeyWords } from './hotkeys.mjs';
4
7
  const has = (record, key) => Object.prototype.hasOwnProperty.call(record, key);
5
8
  /** The kinds of field an expression can give a value to: the rest hold records, lines or files. */
6
9
  const EXPRESSIBLE = new Set(['char', 'text', 'html', 'integer', 'float', 'monetary', 'boolean', 'date', 'datetime', 'selection', 'json']);
@@ -13,6 +16,7 @@ const listed = (names) => {
13
16
  /** The second pass of every check: ids unique, every name defined, every condition readable. */
14
17
  export class ReferenceCheck {
15
18
  page;
19
+ line;
16
20
  issues = [];
17
21
  ids = new Map();
18
22
  /** Where the answers of each saved form placed here go, by name: the path of the part that took it first. */
@@ -20,19 +24,39 @@ export class ReferenceCheck {
20
24
  /** The ids of the tabs and wizard steps a step can go to, and where each goTo and show names one. */
21
25
  places = new Set();
22
26
  goingTo = [];
23
- constructor(page) {
27
+ constructor(page, line) {
24
28
  this.page = page;
29
+ this.line = line;
25
30
  }
26
31
  run() {
27
- this.checkFields(this.page.fields, 'fields');
32
+ this.checkFields(this.page.fields, 'fields', this.line?.name, this.line?.parent);
28
33
  this.walkRoot(this.page.layout, 'layout');
29
34
  if (this.page.on)
30
35
  this.checkEvents(this.page.on, 'on');
36
+ this.checkGoingTo();
37
+ return this.issues;
38
+ }
39
+ /** Each step's goTo and show names a tab or a wizard step of this page. */
40
+ checkGoingTo() {
31
41
  for (const { target, path } of this.goingTo) {
32
42
  if (!this.places.has(target))
33
43
  this.report(path, `no tab or wizard step "${target}" on this page`);
34
44
  }
35
- return this.issues;
45
+ }
46
+ /** Where the page's own expressions read: its fields — on a line's form, the line's, with its record as parent. */
47
+ here() {
48
+ return this.line ? { fields: this.page.fields, lines: this.line.name, parent: this.line.parent } : { fields: this.page.fields };
49
+ }
50
+ /** A line's own form (`lineForm`), its parts checked against the line's fields, its record as parent. */
51
+ checkLineForm(node, def, path) {
52
+ const children = node.lineForm?.children;
53
+ if (!Array.isArray(children))
54
+ return this.report(`${path}.lineForm`, 'a line’s form holds its parts: { "children": [...] }');
55
+ const page = { ...this.page, fields: def.fields, layout: { type: 'sections', id: `${node.id}#line`, children }, on: undefined };
56
+ const inside = new ReferenceCheck(page, { name: node.field, parent: this.page.fields });
57
+ inside.walkChildren(children, `${path}.lineForm`);
58
+ inside.checkGoingTo();
59
+ this.issues.push(...inside.issues);
36
60
  }
37
61
  /** The form's moments: their steps checked, a change named by a field (or a saved form placed here, by its answers' name), a step or tab shown by its id. */
38
62
  checkEvents(on, path) {
@@ -55,6 +79,9 @@ export class ReferenceCheck {
55
79
  checkPress(button, path) {
56
80
  if (!button.steps && button.action === undefined)
57
81
  this.report(path, 'a button needs steps, an action, or both');
82
+ const key = 'hotkey' in button ? button.hotkey : undefined;
83
+ if (typeof key === 'string' && BROWSER_HOTKEYS.includes(key.replace('shift+', '')))
84
+ this.report(`${path}.hotkey`, `${hotkeyWords(key)} is the browser’s own: pick another key`);
58
85
  if (button.steps)
59
86
  this.checkSteps(button.steps, `${path}.steps`);
60
87
  }
@@ -66,7 +93,7 @@ export class ReferenceCheck {
66
93
  * as an expression.
67
94
  */
68
95
  checkSteps(steps, path) {
69
- const scope = { fields: this.page.fields };
96
+ const scope = this.here();
70
97
  steps.forEach((step, i) => {
71
98
  const at = `${path}[${i}]`;
72
99
  this.checkModifiers(step, at, ['when']);
@@ -107,6 +134,8 @@ export class ReferenceCheck {
107
134
  this.checkExpression(step.record, `${at}.record`, scope);
108
135
  for (const [name, value] of Object.entries(step.values ?? {}))
109
136
  this.checkExpression(value, `${at}.values.${name}`, scope);
137
+ // A list's records by this form's values: each valueFrom names one of its fields.
138
+ step.filter?.forEach((item, j) => this.checkFilterItem(item, `${at}.filter[${j}]`, scope));
110
139
  for (const [name, value] of Object.entries(step.into ?? {})) {
111
140
  this.need(name, `${at}.into.${name}`);
112
141
  try {
@@ -119,11 +148,19 @@ export class ReferenceCheck {
119
148
  if (step.then)
120
149
  this.checkSteps(step.then, `${at}.then`);
121
150
  return;
151
+ case 'openUrl':
152
+ this.checkExpression(step.url, `${at}.url`, scope);
153
+ return;
122
154
  default:
123
155
  return;
124
156
  }
125
157
  });
126
158
  }
159
+ /** Words that show fields' values: each `{name}` a field of this page. */
160
+ checkValuesIn(words, path) {
161
+ for (const name of valuesIn(words))
162
+ this.need(name, path);
163
+ }
127
164
  report(path, message) {
128
165
  this.issues.push({ path, message });
129
166
  }
@@ -141,28 +178,94 @@ export class ReferenceCheck {
141
178
  this.report(path, `no field "${name}"`);
142
179
  return undefined;
143
180
  }
181
+ /** A filter's conditions: groups nest, and each valueFrom inside them must name a field too. */
182
+ checkFilterItem(item, at, scope) {
183
+ if ('any' in item)
184
+ return item.any.forEach((inner, i) => this.checkFilterItem(inner, `${at}.any[${i}]`, scope));
185
+ if ('all' in item)
186
+ return item.all.forEach((inner, i) => this.checkFilterItem(inner, `${at}.all[${i}]`, scope));
187
+ if (item.valueFrom !== undefined)
188
+ this.checkValueFrom(item.valueFrom, `${at}.valueFrom`, scope);
189
+ }
190
+ /** A filter's `valueFrom`: a field where it is used, the record's `id`, a part of `user`, or on a line a field of its record. */
191
+ checkValueFrom(name, path, scope) {
192
+ const [root, part] = name.split('.');
193
+ if (part === undefined) {
194
+ if (has(scope.fields, name) || (name === 'id' && scope.lines === undefined))
195
+ return;
196
+ this.need(name, path, scope.fields);
197
+ }
198
+ else if (root === 'user') {
199
+ if (!USER_PARTS.includes(part))
200
+ this.report(path, `"${name}": the person has an id, name or roles`);
201
+ }
202
+ else if (root === 'context') {
203
+ // The app's values: any name it passes in.
204
+ }
205
+ else if (scope.lines === undefined) {
206
+ this.report(path, `"${name}": only a line's filter reads parent, the record it is on`);
207
+ }
208
+ else if (!has(scope.parent ?? this.page.fields, part) && part !== 'id') {
209
+ this.report(path, `"${name}": "${part}" is not a field of this page`);
210
+ }
211
+ }
144
212
  /** Every modifier on `owner` must read, and read only fields of this page (or of its line). */
145
- checkModifiers(owner, path, keys = ['invisible'], scope = { fields: this.page.fields }) {
213
+ checkModifiers(owner, path, keys = ['invisible'], scope = this.here()) {
146
214
  for (const key of keys) {
147
215
  const value = owner[key];
148
216
  if (typeof value !== 'string')
149
217
  continue;
150
- let fields;
218
+ let compiled;
151
219
  try {
152
- fields = compileModifier(value).fields;
220
+ compiled = compileModifier(value);
153
221
  }
154
222
  catch (error) {
155
223
  this.report(`${path}.${key}`, `cannot read "${value}": ${error.message}`);
156
224
  continue;
157
225
  }
158
- this.checkReads(value, fields, `${path}.${key}`, scope);
226
+ this.checkReads(value, compiled, `${path}.${key}`, scope);
159
227
  }
160
228
  }
161
- /** Each name an expression reads must be a field where it is worked out. */
162
- checkReads(source, fields, path, scope) {
163
- for (const name of fields) {
229
+ /**
230
+ * Each name an expression reads must be a field where it is worked out, or
231
+ * on a record what every expression there reads: its `id`, and `user` with
232
+ * the person's `id`, `name` or `roles`.
233
+ */
234
+ checkReads(source, reads, path, scope) {
235
+ // A count's or a sum's condition reads the fields of the lines it is on.
236
+ for (const where of reads.wheres) {
237
+ const lines = scope.fields[where.lines];
238
+ if (lines?.type !== 'one2many')
239
+ continue;
240
+ let fields;
241
+ try {
242
+ fields = compileModifier(where.condition).fields;
243
+ }
244
+ catch (error) {
245
+ this.report(path, `cannot read the condition "${where.condition}": ${error.message}`);
246
+ continue;
247
+ }
248
+ for (const name of fields)
249
+ if (!has(lines.fields, name))
250
+ this.report(path, `"${where.condition}" reads "${name}", which is not a field of the lines of "${where.lines}"`);
251
+ }
252
+ for (const name of reads.fields) {
164
253
  if (has(scope.fields, name))
165
254
  continue;
255
+ // A record's expressions read its id and the person; a line's, its own id, the person and the record it is on.
256
+ const builtIn = scope.lines === undefined ? BUILT_IN_NAMES.includes(name) : name === 'user' || name === 'parent' || name === 'id' || name === 'context';
257
+ if (builtIn) {
258
+ for (const read of reads.paths) {
259
+ const [root, part] = read.split('.');
260
+ if (root !== name || part === undefined)
261
+ continue;
262
+ if (name === 'user' && !USER_PARTS.includes(part))
263
+ this.report(path, `"${source}" reads "${read}": the person has an id, name or roles`);
264
+ if (name === 'parent' && !has(scope.parent ?? this.page.fields, part) && !BUILT_IN_NAMES.includes(part))
265
+ this.report(path, `"${source}" reads "${read}": "${part}" is not a field of this page`);
266
+ }
267
+ continue;
268
+ }
166
269
  const where = scope.lines === undefined ? 'of this page' : `of the lines of "${scope.lines}"`;
167
270
  this.report(path, `"${source}" reads "${name}", which is not a field ${where}`);
168
271
  }
@@ -181,7 +284,7 @@ export class ReferenceCheck {
181
284
  this.report(path, `cannot read "${source}": ${error.message}`);
182
285
  return null;
183
286
  }
184
- this.checkReads(source, compiled.fields, path, scope);
287
+ this.checkReads(source, compiled, path, scope);
185
288
  for (const sum of compiled.sums) {
186
289
  if (!has(scope.fields, sum.lines))
187
290
  continue;
@@ -215,6 +318,7 @@ export class ReferenceCheck {
215
318
  }
216
319
  def.setWhen?.forEach((item, i) => {
217
320
  this.checkModifiers(item, `${path}.setWhen[${i}]`, ['when'], scope);
321
+ item.on?.forEach((other, j) => this.need(other, `${path}.setWhen[${i}].on[${j}]`, fields));
218
322
  this.checkExpression(item.value, `${path}.setWhen[${i}].value`, scope);
219
323
  });
220
324
  }
@@ -224,32 +328,36 @@ export class ReferenceCheck {
224
328
  this.report(`${base}.${cycle[0]}.compute`, message);
225
329
  }
226
330
  }
227
- checkFields(fields, base, lines) {
228
- this.checkWorkedOut(fields, base, { fields, lines });
331
+ checkFields(fields, base, lines, parent) {
332
+ const scope = { fields, lines, ...(parent ? { parent } : {}) };
333
+ this.checkWorkedOut(fields, base, scope);
229
334
  for (const [name, def] of Object.entries(fields)) {
230
335
  const path = `${base}.${name}`;
231
- if (def.type === 'monetary' && def.currencyField !== undefined) {
232
- const currency = this.need(def.currencyField, `${path}.currencyField`, fields);
233
- if (currency && !['many2one', 'selection', 'char'].includes(currency.type)) {
234
- this.report(`${path}.currencyField`, `"${def.currencyField}" is a ${currency.type}; a currency field must be a many2one, selection or char`);
336
+ if (def.type === 'monetary' && def.currencyField !== undefined)
337
+ this.checkCurrency(def.currencyField, `${path}.currencyField`, fields, lines, parent);
338
+ // A first value worked out, and what a record made from a link starts with: read where this field is.
339
+ if (def.defaultFrom !== undefined)
340
+ this.checkExpression(def.defaultFrom, `${path}.defaultFrom`, scope);
341
+ if ((def.type === 'many2one' || def.type === 'many2many') && def.createValues) {
342
+ for (const [key, source] of Object.entries(def.createValues))
343
+ this.checkExpression(source, `${path}.createValues.${key}`, scope);
344
+ }
345
+ // A new line's values: fields of its lines, read from the record.
346
+ if (def.type === 'one2many' && def.lineDefaults) {
347
+ for (const [key, source] of Object.entries(def.lineDefaults)) {
348
+ if (!has(def.fields, key))
349
+ this.report(`${path}.lineDefaults.${key}`, `"${key}" is not a field of the lines of "${name}"`);
350
+ else
351
+ this.checkExpression(source, `${path}.lineDefaults.${key}`, scope);
235
352
  }
236
353
  }
237
354
  if ((def.type === 'many2one' || def.type === 'many2many') && def.filter) {
238
- // Groups nest: each valueFrom inside them must name a field too.
239
- const walk = (item, at) => {
240
- if ('any' in item)
241
- return item.any.forEach((inner, i) => walk(inner, `${at}.any[${i}]`));
242
- if ('all' in item)
243
- return item.all.forEach((inner, i) => walk(inner, `${at}.all[${i}]`));
244
- if (item.valueFrom !== undefined)
245
- this.need(item.valueFrom, `${at}.valueFrom`, fields);
246
- };
247
- def.filter.forEach((item, i) => {
248
- walk(item, `${path}.filter[${i}]`);
249
- });
355
+ def.filter.forEach((item, i) => this.checkFilterItem(item, `${path}.filter[${i}]`, scope));
250
356
  }
251
357
  if (def.type === 'selection')
252
358
  def.optionsFrom?.dependsOn?.forEach((other, i) => this.need(other, `${path}.optionsFrom.dependsOn[${i}]`, fields));
359
+ if (def.type === 'properties')
360
+ def.definitionsFrom?.dependsOn?.forEach((other, i) => this.need(other, `${path}.definitionsFrom.dependsOn[${i}]`, fields));
253
361
  if (def.type === 'properties' && def.definitions) {
254
362
  const seen = new Set();
255
363
  def.definitions.forEach((property, i) => {
@@ -262,8 +370,9 @@ export class ReferenceCheck {
262
370
  });
263
371
  }
264
372
  if (def.type === 'one2many') {
265
- this.checkFields(def.fields, `${path}.fields`, name);
266
- if (def.lineKinds)
373
+ // Its lines read the record they are on — on a line, that line — as parent.
374
+ this.checkFields(def.fields, `${path}.fields`, name, fields);
375
+ if ('lineKinds' in def && def.lineKinds)
267
376
  this.checkLineKinds(name, def.lineKinds, def.fields, `${path}.lineKinds`);
268
377
  if (def.sequenceField !== undefined) {
269
378
  const sequence = def.fields[def.sequenceField];
@@ -276,6 +385,21 @@ export class ReferenceCheck {
276
385
  }
277
386
  }
278
387
  }
388
+ /** A money field's currency: a field beside it, or on a line `parent.` and a field of the record — one that holds a currency. */
389
+ checkCurrency(name, path, fields, lines, parent = this.page.fields) {
390
+ let currency;
391
+ if (!name.startsWith('parent.'))
392
+ currency = this.need(name, path, fields);
393
+ else if (lines === undefined)
394
+ return this.report(path, `"${name}": only a line’s money reads parent, the record it is on`);
395
+ else if (!has(parent, name.slice('parent.'.length)))
396
+ return this.report(path, `"${name}": "${name.slice('parent.'.length)}" is not a field of this page`);
397
+ else
398
+ currency = parent[name.slice('parent.'.length)];
399
+ if (currency && !['many2one', 'selection', 'char'].includes(currency.type)) {
400
+ this.report(path, `"${name}" is a ${currency.type}; a currency field must be a many2one, selection or char`);
401
+ }
402
+ }
279
403
  checkLineKinds(field, kinds, lineFields, path) {
280
404
  const kindField = lineFields[kinds.field];
281
405
  if (!kindField)
@@ -297,10 +421,20 @@ export class ReferenceCheck {
297
421
  this.report(`${path}.text`, `"${kinds.text}" is a ${text.type}; a section's or note's text must be a char or text field`);
298
422
  }
299
423
  }
424
+ /** A page's own buttons at its foot: buttons as any. */
425
+ walkFooter(footer, path) {
426
+ footer?.forEach((button, i) => {
427
+ this.claim(button.id, `${path}.footer[${i}]`);
428
+ this.checkModifiers(button, `${path}.footer[${i}]`);
429
+ this.checkPress(button, `${path}.footer[${i}]`);
430
+ });
431
+ }
300
432
  walkRoot(root, path) {
301
433
  if (root.type === 'tabs')
302
434
  return this.walkNode(root, path);
303
435
  this.claim(root.id, path);
436
+ if (root.type === 'sheet' || root.type === 'sections')
437
+ this.walkFooter(root.footer, path);
304
438
  if (root.type === 'sheet')
305
439
  return this.walkSheet(root, path);
306
440
  if (root.type === 'sections')
@@ -361,6 +495,16 @@ export class ReferenceCheck {
361
495
  this.report(path, 'a collapsible section needs a title to fold it by');
362
496
  if (node.collapsed && !node.collapsible)
363
497
  this.report(path, 'collapsed needs collapsible: true');
498
+ if (node.style === 'inline' && node.columns !== undefined)
499
+ this.report(`${path}.columns`, 'parts on one line take no columns: each is as wide as it needs');
500
+ if (node.style === 'inline' && node.collapsible)
501
+ this.report(`${path}.collapsible`, 'a line of parts does not fold: its title is its label');
502
+ if (node.style === 'inline') {
503
+ node.children.forEach((child, i) => {
504
+ if (!['field', 'text', 'button', 'spacer'].includes(child.type))
505
+ this.report(`${path}.children[${i}]`, `a line of parts holds fields, words and buttons; a ${child.type} needs a row of its own`);
506
+ });
507
+ }
364
508
  return this.walkChildren(node.children, path);
365
509
  case 'tabs':
366
510
  return this.walkTabs(node, path);
@@ -368,6 +512,8 @@ export class ReferenceCheck {
368
512
  return this.checkFormNode(node, path);
369
513
  case 'button':
370
514
  return this.checkPress(node, path);
515
+ case 'text':
516
+ return this.checkValuesIn(node.text, `${path}.text`);
371
517
  default:
372
518
  return;
373
519
  }
@@ -405,10 +551,22 @@ export class ReferenceCheck {
405
551
  this.need(sheet.title.subtitleField, `${path}.title.subtitleField`);
406
552
  if (sheet.title.avatarField !== undefined)
407
553
  this.need(sheet.title.avatarField, `${path}.title.avatarField`);
554
+ sheet.title.placeholderWhen?.forEach((item, i) => this.checkModifiers(item, `${path}.title.placeholderWhen[${i}]`, ['when']));
408
555
  }
409
556
  if (sheet.statusbar) {
410
- const { field, visibleStates } = sheet.statusbar;
557
+ const { field, visibleStates, durationsField, fold, saves, clickable } = sheet.statusbar;
558
+ const at = `${path}.statusbar`;
559
+ this.checkModifiers(sheet.statusbar, at);
560
+ if (durationsField !== undefined) {
561
+ const times = this.need(durationsField, `${at}.durationsField`);
562
+ if (times && times.type !== 'json')
563
+ this.report(`${at}.durationsField`, `"${durationsField}" is a ${times.type}; time per step comes from a json field`);
564
+ }
565
+ if (saves && !clickable)
566
+ this.report(`${at}.saves`, 'a click saves only where a step can be clicked (clickable)');
411
567
  const def = this.need(field, `${path}.statusbar.field`);
568
+ if (fold && def && def.type !== 'many2one')
569
+ this.report(`${at}.fold`, 'steps fold by their record (a stage), so only a link’s steps fold; a choice keeps the steps it shows with visibleStates');
412
570
  if (def && def.type !== 'selection' && def.type !== 'many2one') {
413
571
  this.report(`${path}.statusbar.field`, `"${field}" is a ${def.type}; a statusbar needs a selection or many2one`);
414
572
  }
@@ -430,24 +588,60 @@ export class ReferenceCheck {
430
588
  this.claim(stat.id, `${path}.statButtons[${i}]`);
431
589
  this.checkModifiers(stat, `${path}.statButtons[${i}]`);
432
590
  this.checkPress(stat, `${path}.statButtons[${i}]`);
433
- if (stat.field !== undefined)
434
- this.need(stat.field, `${path}.statButtons[${i}].field`);
591
+ for (const key of ['field', 'unitField', 'labelField', 'secondField']) {
592
+ const name = stat[key];
593
+ if (name !== undefined)
594
+ this.need(name, `${path}.statButtons[${i}].${key}`);
595
+ }
596
+ if (stat.secondLabel !== undefined && stat.secondField === undefined)
597
+ this.report(`${path}.statButtons[${i}].secondLabel`, 'words for a second value need its field (secondField)');
435
598
  });
436
- if (sheet.ribbon) {
437
- this.claim(sheet.ribbon.id, `${path}.ribbon`);
438
- this.checkModifiers(sheet.ribbon, `${path}.ribbon`);
599
+ const ribbons = [...(sheet.ribbon ? [[sheet.ribbon, `${path}.ribbon`]] : []), ...(sheet.ribbons ?? []).map((ribbon, i) => [ribbon, `${path}.ribbons[${i}]`])];
600
+ for (const [ribbon, at] of ribbons) {
601
+ this.claim(ribbon.id, at);
602
+ this.checkModifiers(ribbon, at);
603
+ if (ribbon.labelField !== undefined)
604
+ this.need(ribbon.labelField, `${at}.labelField`);
439
605
  }
440
606
  sheet.badges?.forEach((badge, i) => {
441
607
  this.claim(badge.id, `${path}.badges[${i}]`);
442
608
  this.checkModifiers(badge, `${path}.badges[${i}]`);
443
609
  });
444
- for (const place of ['above', 'below']) {
610
+ for (const place of ['above', 'before', 'after', 'below']) {
445
611
  sheet.title?.[place]?.forEach((node, i) => this.walkNode(node, `${path}.title.${place}[${i}]`));
446
612
  }
447
613
  sheet.alerts?.forEach((alert, i) => {
448
- this.claim(alert.id, `${path}.alerts[${i}]`);
449
- this.checkModifiers(alert, `${path}.alerts[${i}]`);
614
+ const at = `${path}.alerts[${i}]`;
615
+ this.claim(alert.id, at);
616
+ this.checkModifiers(alert, at);
617
+ this.checkValuesIn(alert.message, `${at}.message`);
618
+ if (alert.messageField !== undefined)
619
+ this.need(alert.messageField, `${at}.messageField`);
620
+ alert.buttons?.forEach((button, j) => {
621
+ this.claim(button.id, `${at}.buttons[${j}]`);
622
+ this.checkModifiers(button, `${at}.buttons[${j}]`);
623
+ this.checkPress(button, `${at}.buttons[${j}]`);
624
+ });
625
+ });
626
+ // The gear menu: buttons of the record, each doing something — its steps, an action, or a built-in — and named.
627
+ sheet.toolbar?.menu?.forEach((item, i) => {
628
+ const at = `${path}.toolbar.menu[${i}]`;
629
+ this.claim(item.id, at);
630
+ this.checkModifiers(item, at);
631
+ if (!item.steps && item.action === undefined && !item.builtin)
632
+ this.report(at, 'a menu item needs steps, an action, or a built-in');
633
+ if (item.label === undefined && !item.builtin)
634
+ this.report(`${at}.label`, 'a menu item needs words, unless it is a built-in');
635
+ if (item.steps)
636
+ this.checkSteps(item.steps, `${at}.steps`);
450
637
  });
638
+ const preview = sheet.attachmentPreview;
639
+ if (preview) {
640
+ this.checkModifiers(preview, `${path}.attachmentPreview`);
641
+ const def = preview.field !== undefined ? this.need(preview.field, `${path}.attachmentPreview.field`) : undefined;
642
+ if (def && def.type !== 'binary' && def.type !== 'image')
643
+ this.report(`${path}.attachmentPreview.field`, `"${preview.field}" is a ${def.type}; the preview shows a file (binary or image)`);
644
+ }
451
645
  this.walkChildren(sheet.children, path);
452
646
  if (sheet.sidePanel) {
453
647
  this.claim(sheet.sidePanel.id, `${path}.sidePanel`);
@@ -460,6 +654,13 @@ export class ReferenceCheck {
460
654
  node.validate?.forEach((rule, i) => {
461
655
  const at = `${path}.validate[${i}]`;
462
656
  this.checkModifiers(rule, at, ['when', 'holds']);
657
+ // No value twice in a column: a column of this table's lines.
658
+ if (rule.distinct !== undefined && def) {
659
+ if (def.type !== 'one2many')
660
+ this.report(`${at}.distinct`, `distinct is for a table of lines; "${node.field}" is a ${def.type}`);
661
+ else if (!has(def.fields, rule.distinct))
662
+ this.report(`${at}.distinct`, `"${rule.distinct}" is not a field of the lines of "${node.field}"`);
663
+ }
463
664
  if (rule.pattern === undefined)
464
665
  return;
465
666
  try {
@@ -469,6 +670,10 @@ export class ReferenceCheck {
469
670
  this.report(`${at}.pattern`, `"${rule.pattern}" is not a regular expression`);
470
671
  }
471
672
  });
673
+ // Its value's tones and bold, and the words its empty box shows: read on the record, as its other conditions are.
674
+ node.tones?.forEach((tone, i) => this.checkModifiers(tone, `${path}.tones[${i}]`, ['when']));
675
+ node.placeholderWhen?.forEach((item, i) => this.checkModifiers(item, `${path}.placeholderWhen[${i}]`, ['when']));
676
+ this.checkModifiers(node, path, ['bold']);
472
677
  if (def && node.totals) {
473
678
  if (def.type !== 'one2many') {
474
679
  this.report(`${path}.totals`, `totals only apply to one2many fields; "${node.field}" is a ${def.type}`);
@@ -489,16 +694,79 @@ export class ReferenceCheck {
489
694
  if (key.endsWith('Field') && typeof value === 'string')
490
695
  this.need(value, `${path}.options.${key}`);
491
696
  }
697
+ // The order lines load in: fields of its lines.
698
+ if (def && node.order) {
699
+ if (def.type !== 'one2many')
700
+ this.report(`${path}.order`, `order only applies to one2many fields; "${node.field}" is a ${def.type}`);
701
+ else
702
+ node.order.forEach((item, i) => !has(def.fields, item.field) && this.report(`${path}.order[${i}].field`, `"${item.field}" is not a field of the lines of "${node.field}"`));
703
+ }
704
+ // A line's own form: its parts read the line's fields, the record as parent.
705
+ if (def && node.lineForm) {
706
+ if (def.type !== 'one2many')
707
+ this.report(`${path}.lineForm`, `lineForm only applies to one2many fields; "${node.field}" is a ${def.type}`);
708
+ else
709
+ this.checkLineForm(node, def, path);
710
+ }
711
+ for (const key of ['lineOpens', 'cards', 'fit']) {
712
+ if (def && node[key] && def.type !== 'one2many')
713
+ this.report(`${path}.${key}`, `${key} only applies to one2many fields; "${node.field}" is a ${def.type}`);
714
+ }
492
715
  if (def && node.editMode && def.type !== 'one2many') {
493
716
  this.report(`${path}.editMode`, `editMode only applies to one2many fields; "${node.field}" is a ${def.type}`);
494
717
  }
718
+ // A table's own rules: a cell's read on its line (with parent), a column's `hidden` on the record.
719
+ if (def && node.lineDelete !== undefined && def.type !== 'one2many')
720
+ this.report(`${path}.lineDelete`, `lineDelete only applies to one2many fields; "${node.field}" is a ${def.type}`);
721
+ if (def && (node.cells || node.rowTones || node.rowBold !== undefined || node.rowButtons || node.lineDelete !== undefined)) {
722
+ if (def.type !== 'one2many') {
723
+ if (node.cells || node.rowTones || node.rowBold !== undefined || node.rowButtons)
724
+ this.report(path, `cells, rowTones, rowBold and rowButtons only apply to one2many fields; "${node.field}" is a ${def.type}`);
725
+ }
726
+ else {
727
+ const line = { fields: def.fields, lines: node.field };
728
+ for (const [column, rules] of Object.entries(node.cells ?? {})) {
729
+ if (!has(def.fields, column)) {
730
+ this.report(`${path}.cells.${column}`, `"${column}" is not a field of the lines of "${node.field}"`);
731
+ continue;
732
+ }
733
+ this.checkModifiers(rules, `${path}.cells.${column}`, ['invisible', 'readonly', 'required', 'bold'], line);
734
+ this.checkModifiers(rules, `${path}.cells.${column}`, ['hidden']);
735
+ rules.tones?.forEach((tone, i) => this.checkModifiers(tone, `${path}.cells.${column}.tones[${i}]`, ['when'], line));
736
+ // Its widget's options that point at a field: one of the line's.
737
+ for (const [key, value] of Object.entries(rules.options ?? {})) {
738
+ if (key.endsWith('Field') && typeof value === 'string')
739
+ this.need(value, `${path}.cells.${column}.options.${key}`, def.fields);
740
+ }
741
+ }
742
+ node.rowTones?.forEach((tone, i) => this.checkModifiers(tone, `${path}.rowTones[${i}]`, ['when'], line));
743
+ this.checkModifiers(node, path, ['rowBold', 'lineDelete'], line);
744
+ node.rowButtons?.forEach((button, i) => {
745
+ this.claim(button.id, `${path}.rowButtons[${i}]`);
746
+ this.checkModifiers(button, `${path}.rowButtons[${i}]`, ['invisible'], line);
747
+ this.checkPress(button, `${path}.rowButtons[${i}]`);
748
+ });
749
+ }
750
+ }
751
+ // Buttons for the chosen lines and in the control row: buttons of the record, as any.
752
+ for (const key of ['selectedButtons', 'controlButtons']) {
753
+ if (!node[key])
754
+ continue;
755
+ if (def && def.type !== 'one2many')
756
+ this.report(`${path}.${key}`, `${key} only apply to one2many fields; "${node.field}" is a ${def.type}`);
757
+ node[key].forEach((button, i) => {
758
+ this.claim(button.id, `${path}.${key}[${i}]`);
759
+ this.checkModifiers(button, `${path}.${key}[${i}]`);
760
+ this.checkPress(button, `${path}.${key}[${i}]`);
761
+ });
762
+ }
495
763
  if (def && node.optionalColumns) {
496
764
  if (def.type !== 'one2many') {
497
765
  this.report(`${path}.optionalColumns`, `optional columns only apply to one2many fields; "${node.field}" is a ${def.type}`);
498
766
  }
499
767
  else {
500
768
  // The columns the table shows: those named, or every line field but the structural ones.
501
- const shown = node.columns ?? Object.keys(def.fields).filter((c) => c !== def.lineKinds?.field && c !== def.sequenceField);
769
+ const shown = node.columns ?? Object.keys(def.fields).filter((c) => c !== ('lineKinds' in def ? def.lineKinds?.field : undefined) && c !== def.sequenceField);
502
770
  for (const column of Object.keys(node.optionalColumns)) {
503
771
  if (!shown.includes(column))
504
772
  this.report(`${path}.optionalColumns.${column}`, `"${column}" is not a column of "${node.field}"`);
@@ -512,9 +780,19 @@ export class ReferenceCheck {
512
780
  if (!has(def.fields, column)) {
513
781
  this.report(`${path}.columns[${i}]`, `"${column}" is not a field of the lines of "${node.field}"`);
514
782
  }
783
+ else if (def.fields[column].type === 'one2many') {
784
+ this.report(`${path}.columns[${i}]`, `"${column}" holds lines of its own: they are edited in the line's form (lineForm), not drawn in its row`);
785
+ }
786
+ });
787
+ }
788
+ else if (def.type === 'many2many') {
789
+ // A table of the records linked: its columns are fields of those records.
790
+ node.columns.forEach((column, i) => {
791
+ if (!def.fields || !has(def.fields, column))
792
+ this.report(`${path}.columns[${i}]`, `"${column}" is not a field of the records of "${node.field}"`);
515
793
  });
516
794
  }
517
- else if (def.type !== 'many2many') {
795
+ else {
518
796
  this.report(`${path}.columns`, `columns only apply to one2many and many2many fields; "${node.field}" is a ${def.type}`);
519
797
  }
520
798
  }