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