@fieldia/core 0.10.0 → 0.12.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 (205) hide show
  1. package/README.md +204 -0
  2. package/package.json +1 -1
  3. package/schema/page.schema.json +4700 -904
  4. package/src/index.d.mts +2 -1
  5. package/src/index.d.ts +2 -1
  6. package/src/index.js +7 -1
  7. package/src/index.js.map +1 -1
  8. package/src/index.mjs +2 -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 +1176 -0
  51. package/src/lib/format/actions.d.ts +1176 -0
  52. package/src/lib/format/actions.js +62 -0
  53. package/src/lib/format/actions.js.map +1 -0
  54. package/src/lib/format/actions.mjs +59 -0
  55. package/src/lib/format/actions.mjs.map +1 -0
  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 +105 -16
  65. package/src/lib/format/field.js.map +1 -1
  66. package/src/lib/format/field.mjs +104 -15
  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 +4773 -211
  75. package/src/lib/format/layout.d.ts +4773 -211
  76. package/src/lib/format/layout.js +147 -21
  77. package/src/lib/format/layout.js.map +1 -1
  78. package/src/lib/format/layout.mjs +146 -20
  79. package/src/lib/format/layout.mjs.map +1 -1
  80. package/src/lib/format/layout.types.d.mts +2 -1
  81. package/src/lib/format/layout.types.d.ts +2 -1
  82. package/src/lib/format/page.d.mts +3563 -222
  83. package/src/lib/format/page.d.ts +3563 -222
  84. package/src/lib/format/page.js +19 -2
  85. package/src/lib/format/page.js.map +1 -1
  86. package/src/lib/format/page.mjs +20 -3
  87. package/src/lib/format/page.mjs.map +1 -1
  88. package/src/lib/format/part-look.d.mts +36 -0
  89. package/src/lib/format/part-look.d.ts +36 -0
  90. package/src/lib/format/part-look.js +23 -0
  91. package/src/lib/format/part-look.js.map +1 -0
  92. package/src/lib/format/part-look.mjs +20 -0
  93. package/src/lib/format/part-look.mjs.map +1 -0
  94. package/src/lib/format/references.d.mts +54 -2
  95. package/src/lib/format/references.d.ts +54 -2
  96. package/src/lib/format/references.js +450 -43
  97. package/src/lib/format/references.js.map +1 -1
  98. package/src/lib/format/references.mjs +450 -43
  99. package/src/lib/format/references.mjs.map +1 -1
  100. package/src/lib/format/translate.js +5 -3
  101. package/src/lib/format/translate.js.map +1 -1
  102. package/src/lib/format/translate.mjs +5 -3
  103. package/src/lib/format/translate.mjs.map +1 -1
  104. package/src/lib/format/words.d.mts +9 -0
  105. package/src/lib/format/words.d.ts +9 -0
  106. package/src/lib/format/words.js +35 -0
  107. package/src/lib/format/words.js.map +1 -0
  108. package/src/lib/format/words.mjs +30 -0
  109. package/src/lib/format/words.mjs.map +1 -0
  110. package/src/lib/record/cells.d.mts +37 -0
  111. package/src/lib/record/cells.d.ts +37 -0
  112. package/src/lib/record/cells.js +59 -0
  113. package/src/lib/record/cells.js.map +1 -0
  114. package/src/lib/record/cells.mjs +54 -0
  115. package/src/lib/record/cells.mjs.map +1 -0
  116. package/src/lib/record/compute.d.mts +5 -4
  117. package/src/lib/record/compute.d.ts +5 -4
  118. package/src/lib/record/compute.js +11 -10
  119. package/src/lib/record/compute.js.map +1 -1
  120. package/src/lib/record/compute.mjs +12 -11
  121. package/src/lib/record/compute.mjs.map +1 -1
  122. package/src/lib/record/data-source.d.mts +37 -3
  123. package/src/lib/record/data-source.d.ts +37 -3
  124. package/src/lib/record/data-source.js.map +1 -1
  125. package/src/lib/record/data-source.mjs.map +1 -1
  126. package/src/lib/record/env.d.mts +38 -1
  127. package/src/lib/record/env.d.ts +38 -1
  128. package/src/lib/record/env.js +41 -3
  129. package/src/lib/record/env.js.map +1 -1
  130. package/src/lib/record/env.mjs +38 -3
  131. package/src/lib/record/env.mjs.map +1 -1
  132. package/src/lib/record/filter.d.mts +10 -0
  133. package/src/lib/record/filter.d.ts +10 -0
  134. package/src/lib/record/filter.js +49 -0
  135. package/src/lib/record/filter.js.map +1 -1
  136. package/src/lib/record/filter.mjs +48 -0
  137. package/src/lib/record/filter.mjs.map +1 -1
  138. package/src/lib/record/form.d.mts +226 -25
  139. package/src/lib/record/form.d.ts +226 -25
  140. package/src/lib/record/form.js +785 -83
  141. package/src/lib/record/form.js.map +1 -1
  142. package/src/lib/record/form.mjs +787 -85
  143. package/src/lib/record/form.mjs.map +1 -1
  144. package/src/lib/record/locales/ar.js +3 -0
  145. package/src/lib/record/locales/ar.js.map +1 -1
  146. package/src/lib/record/locales/ar.mjs +3 -0
  147. package/src/lib/record/locales/ar.mjs.map +1 -1
  148. package/src/lib/record/locales/de.js +3 -0
  149. package/src/lib/record/locales/de.js.map +1 -1
  150. package/src/lib/record/locales/de.mjs +3 -0
  151. package/src/lib/record/locales/de.mjs.map +1 -1
  152. package/src/lib/record/locales/fr.js +3 -0
  153. package/src/lib/record/locales/fr.js.map +1 -1
  154. package/src/lib/record/locales/fr.mjs +3 -0
  155. package/src/lib/record/locales/fr.mjs.map +1 -1
  156. package/src/lib/record/memory-data-source.d.mts +26 -2
  157. package/src/lib/record/memory-data-source.d.ts +26 -2
  158. package/src/lib/record/memory-data-source.js +98 -7
  159. package/src/lib/record/memory-data-source.js.map +1 -1
  160. package/src/lib/record/memory-data-source.mjs +98 -7
  161. package/src/lib/record/memory-data-source.mjs.map +1 -1
  162. package/src/lib/record/messages.d.mts +5 -0
  163. package/src/lib/record/messages.d.ts +5 -0
  164. package/src/lib/record/messages.js +3 -0
  165. package/src/lib/record/messages.js.map +1 -1
  166. package/src/lib/record/messages.mjs +3 -0
  167. package/src/lib/record/messages.mjs.map +1 -1
  168. package/src/lib/record/roles.d.mts +10 -0
  169. package/src/lib/record/roles.d.ts +10 -0
  170. package/src/lib/record/roles.js +21 -0
  171. package/src/lib/record/roles.js.map +1 -0
  172. package/src/lib/record/roles.mjs +18 -0
  173. package/src/lib/record/roles.mjs.map +1 -0
  174. package/src/lib/record/rules.js +30 -0
  175. package/src/lib/record/rules.js.map +1 -1
  176. package/src/lib/record/rules.mjs +31 -1
  177. package/src/lib/record/rules.mjs.map +1 -1
  178. package/src/lib/record/run.d.mts +248 -0
  179. package/src/lib/record/run.d.ts +248 -0
  180. package/src/lib/record/run.js +455 -0
  181. package/src/lib/record/run.js.map +1 -0
  182. package/src/lib/record/run.mjs +450 -0
  183. package/src/lib/record/run.mjs.map +1 -0
  184. package/src/lib/record/set-when.d.mts +5 -2
  185. package/src/lib/record/set-when.d.ts +5 -2
  186. package/src/lib/record/set-when.js +28 -12
  187. package/src/lib/record/set-when.js.map +1 -1
  188. package/src/lib/record/set-when.mjs +30 -14
  189. package/src/lib/record/set-when.mjs.map +1 -1
  190. package/src/lib/record/value-map.d.mts +41 -0
  191. package/src/lib/record/value-map.d.ts +41 -0
  192. package/src/lib/record/value-map.js +80 -0
  193. package/src/lib/record/value-map.js.map +1 -0
  194. package/src/lib/record/value-map.mjs +73 -0
  195. package/src/lib/record/value-map.mjs.map +1 -0
  196. package/src/lib/record/values.d.mts +10 -0
  197. package/src/lib/record/values.d.ts +10 -0
  198. package/src/lib/record/values.js.map +1 -1
  199. package/src/lib/record/values.mjs.map +1 -1
  200. package/src/runtime.d.mts +11 -4
  201. package/src/runtime.d.ts +11 -4
  202. package/src/runtime.js +13 -1
  203. package/src/runtime.js.map +1 -1
  204. package/src/runtime.mjs +4 -0
  205. 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,16 +16,151 @@ 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
- constructor(page) {
22
+ /** Where the answers of each saved form placed here go, by name: the path of the part that took it first. */
23
+ formNames = new Map();
24
+ /** The ids of the tabs and wizard steps a step can go to, and where each goTo and show names one. */
25
+ places = new Set();
26
+ goingTo = [];
27
+ constructor(page, line) {
19
28
  this.page = page;
29
+ this.line = line;
20
30
  }
21
31
  run() {
22
- this.checkFields(this.page.fields, 'fields');
32
+ this.checkFields(this.page.fields, 'fields', this.line?.name, this.line?.parent);
23
33
  this.walkRoot(this.page.layout, 'layout');
34
+ if (this.page.on)
35
+ this.checkEvents(this.page.on, 'on');
36
+ this.checkGoingTo();
24
37
  return this.issues;
25
38
  }
39
+ /** Each step's goTo and show names a tab or a wizard step of this page. */
40
+ checkGoingTo() {
41
+ for (const { target, path } of this.goingTo) {
42
+ if (!this.places.has(target))
43
+ this.report(path, `no tab or wizard step "${target}" on this page`);
44
+ }
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);
60
+ }
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. */
62
+ checkEvents(on, path) {
63
+ for (const moment of ['open', 'beforeSave', 'afterSave']) {
64
+ const steps = on[moment];
65
+ if (steps)
66
+ this.checkSteps(steps, `${path}.${moment}`);
67
+ }
68
+ for (const [field, steps] of Object.entries(on.change ?? {})) {
69
+ if (!this.formNames.has(field))
70
+ this.need(field, `${path}.change.${field}`);
71
+ this.checkSteps(steps, `${path}.change.${field}`);
72
+ }
73
+ for (const [target, steps] of Object.entries(on.show ?? {})) {
74
+ this.goingTo.push({ target, path: `${path}.show.${target}` });
75
+ this.checkSteps(steps, `${path}.show.${target}`);
76
+ }
77
+ }
78
+ /** A press: steps, an app action, or both — and its steps checked. */
79
+ checkPress(button, path) {
80
+ if (!button.steps && button.action === undefined)
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`);
85
+ if (button.steps)
86
+ this.checkSteps(button.steps, `${path}.steps`);
87
+ }
88
+ /**
89
+ * Steps, each against this page: the fields it sets, empties, checks or adds
90
+ * a line to are this page's; its expressions read; a step it goes to is a
91
+ * tab or a wizard step here. The opened page's own fields are not known
92
+ * here: what it starts with is named freely, and what comes back is read
93
+ * as an expression.
94
+ */
95
+ checkSteps(steps, path) {
96
+ const scope = this.here();
97
+ steps.forEach((step, i) => {
98
+ const at = `${path}[${i}]`;
99
+ this.checkModifiers(step, at, ['when']);
100
+ switch (step.do) {
101
+ case 'set': {
102
+ const def = this.need(step.field, `${at}.field`);
103
+ if (def && !EXPRESSIBLE.has(def.type))
104
+ this.report(`${at}.field`, `a ${def.type} cannot be set from an expression; ${EXPRESSIBLE_WORDS}`);
105
+ this.checkExpression(step.value, `${at}.value`, scope);
106
+ return;
107
+ }
108
+ case 'clear':
109
+ this.need(step.field, `${at}.field`);
110
+ return;
111
+ case 'addLine': {
112
+ const def = this.need(step.field, `${at}.field`);
113
+ if (def && def.type !== 'one2many')
114
+ return this.report(`${at}.field`, `"${step.field}" is a ${def.type}; a line is added to a one2many`);
115
+ for (const [name, value] of Object.entries(step.values ?? {})) {
116
+ if (def?.type === 'one2many' && !has(def.fields, name))
117
+ this.report(`${at}.values.${name}`, `"${name}" is not a field of the lines of "${step.field}"`);
118
+ this.checkExpression(value, `${at}.values.${name}`, scope);
119
+ }
120
+ return;
121
+ }
122
+ case 'check':
123
+ step.fields?.forEach((field, j) => this.need(field, `${at}.fields[${j}]`));
124
+ return;
125
+ case 'goTo':
126
+ this.goingTo.push({ target: step.target, path: `${at}.target` });
127
+ return;
128
+ case 'open':
129
+ if (step.page === this.page.id && step.as === 'page')
130
+ this.report(`${at}.page`, 'a page cannot open itself in its own place');
131
+ if (step.side !== undefined && step.as !== 'panel')
132
+ this.report(`${at}.side`, `a side is where a panel comes from; this opens ${step.as === 'page' ? 'in its place' : 'in a dialog'}`);
133
+ if (step.record !== undefined)
134
+ this.checkExpression(step.record, `${at}.record`, scope);
135
+ for (const [name, value] of Object.entries(step.values ?? {}))
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));
139
+ for (const [name, value] of Object.entries(step.into ?? {})) {
140
+ this.need(name, `${at}.into.${name}`);
141
+ try {
142
+ compileExpression(value);
143
+ }
144
+ catch (error) {
145
+ this.report(`${at}.into.${name}`, `cannot read "${value}": ${error.message}`);
146
+ }
147
+ }
148
+ if (step.then)
149
+ this.checkSteps(step.then, `${at}.then`);
150
+ return;
151
+ case 'openUrl':
152
+ this.checkExpression(step.url, `${at}.url`, scope);
153
+ return;
154
+ default:
155
+ return;
156
+ }
157
+ });
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
+ }
26
164
  report(path, message) {
27
165
  this.issues.push({ path, message });
28
166
  }
@@ -40,28 +178,94 @@ export class ReferenceCheck {
40
178
  this.report(path, `no field "${name}"`);
41
179
  return undefined;
42
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
+ }
43
212
  /** Every modifier on `owner` must read, and read only fields of this page (or of its line). */
44
- checkModifiers(owner, path, keys = ['invisible'], scope = { fields: this.page.fields }) {
213
+ checkModifiers(owner, path, keys = ['invisible'], scope = this.here()) {
45
214
  for (const key of keys) {
46
215
  const value = owner[key];
47
216
  if (typeof value !== 'string')
48
217
  continue;
49
- let fields;
218
+ let compiled;
50
219
  try {
51
- fields = compileModifier(value).fields;
220
+ compiled = compileModifier(value);
52
221
  }
53
222
  catch (error) {
54
223
  this.report(`${path}.${key}`, `cannot read "${value}": ${error.message}`);
55
224
  continue;
56
225
  }
57
- this.checkReads(value, fields, `${path}.${key}`, scope);
226
+ this.checkReads(value, compiled, `${path}.${key}`, scope);
58
227
  }
59
228
  }
60
- /** Each name an expression reads must be a field where it is worked out. */
61
- checkReads(source, fields, path, scope) {
62
- 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) {
63
253
  if (has(scope.fields, name))
64
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
+ }
65
269
  const where = scope.lines === undefined ? 'of this page' : `of the lines of "${scope.lines}"`;
66
270
  this.report(path, `"${source}" reads "${name}", which is not a field ${where}`);
67
271
  }
@@ -80,7 +284,7 @@ export class ReferenceCheck {
80
284
  this.report(path, `cannot read "${source}": ${error.message}`);
81
285
  return null;
82
286
  }
83
- this.checkReads(source, compiled.fields, path, scope);
287
+ this.checkReads(source, compiled, path, scope);
84
288
  for (const sum of compiled.sums) {
85
289
  if (!has(scope.fields, sum.lines))
86
290
  continue;
@@ -114,6 +318,7 @@ export class ReferenceCheck {
114
318
  }
115
319
  def.setWhen?.forEach((item, i) => {
116
320
  this.checkModifiers(item, `${path}.setWhen[${i}]`, ['when'], scope);
321
+ item.on?.forEach((other, j) => this.need(other, `${path}.setWhen[${i}].on[${j}]`, fields));
117
322
  this.checkExpression(item.value, `${path}.setWhen[${i}].value`, scope);
118
323
  });
119
324
  }
@@ -123,32 +328,36 @@ export class ReferenceCheck {
123
328
  this.report(`${base}.${cycle[0]}.compute`, message);
124
329
  }
125
330
  }
126
- checkFields(fields, base, lines) {
127
- 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);
128
334
  for (const [name, def] of Object.entries(fields)) {
129
335
  const path = `${base}.${name}`;
130
- if (def.type === 'monetary' && def.currencyField !== undefined) {
131
- const currency = this.need(def.currencyField, `${path}.currencyField`, fields);
132
- if (currency && !['many2one', 'selection', 'char'].includes(currency.type)) {
133
- 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);
134
352
  }
135
353
  }
136
354
  if ((def.type === 'many2one' || def.type === 'many2many') && def.filter) {
137
- // Groups nest: each valueFrom inside them must name a field too.
138
- const walk = (item, at) => {
139
- if ('any' in item)
140
- return item.any.forEach((inner, i) => walk(inner, `${at}.any[${i}]`));
141
- if ('all' in item)
142
- return item.all.forEach((inner, i) => walk(inner, `${at}.all[${i}]`));
143
- if (item.valueFrom !== undefined)
144
- this.need(item.valueFrom, `${at}.valueFrom`, fields);
145
- };
146
- def.filter.forEach((item, i) => {
147
- walk(item, `${path}.filter[${i}]`);
148
- });
355
+ def.filter.forEach((item, i) => this.checkFilterItem(item, `${path}.filter[${i}]`, scope));
149
356
  }
150
357
  if (def.type === 'selection')
151
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));
152
361
  if (def.type === 'properties' && def.definitions) {
153
362
  const seen = new Set();
154
363
  def.definitions.forEach((property, i) => {
@@ -161,8 +370,9 @@ export class ReferenceCheck {
161
370
  });
162
371
  }
163
372
  if (def.type === 'one2many') {
164
- this.checkFields(def.fields, `${path}.fields`, name);
165
- 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)
166
376
  this.checkLineKinds(name, def.lineKinds, def.fields, `${path}.lineKinds`);
167
377
  if (def.sequenceField !== undefined) {
168
378
  const sequence = def.fields[def.sequenceField];
@@ -175,6 +385,21 @@ export class ReferenceCheck {
175
385
  }
176
386
  }
177
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
+ }
178
403
  checkLineKinds(field, kinds, lineFields, path) {
179
404
  const kindField = lineFields[kinds.field];
180
405
  if (!kindField)
@@ -196,10 +421,20 @@ export class ReferenceCheck {
196
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`);
197
422
  }
198
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
+ }
199
432
  walkRoot(root, path) {
200
433
  if (root.type === 'tabs')
201
434
  return this.walkNode(root, path);
202
435
  this.claim(root.id, path);
436
+ if (root.type === 'sheet' || root.type === 'sections')
437
+ this.walkFooter(root.footer, path);
203
438
  if (root.type === 'sheet')
204
439
  return this.walkSheet(root, path);
205
440
  if (root.type === 'sections')
@@ -208,6 +443,7 @@ export class ReferenceCheck {
208
443
  return this.walkList(root, path);
209
444
  root.children.forEach((step, i) => {
210
445
  const stepPath = `${path}.children[${i}]`;
446
+ this.places.add(step.id);
211
447
  this.claim(step.id, stepPath);
212
448
  this.checkModifiers(step, stepPath);
213
449
  this.walkChildren(step.children, stepPath);
@@ -240,14 +476,17 @@ export class ReferenceCheck {
240
476
  if (!named.has(id))
241
477
  this.report(`${path}.defaultFilters[${i}]`, `no filter "${id}" in this list`);
242
478
  });
243
- list.actions?.forEach((button, i) => this.claim(button.id, `${path}.actions[${i}]`));
479
+ list.actions?.forEach((button, i) => {
480
+ this.claim(button.id, `${path}.actions[${i}]`);
481
+ this.checkPress(button, `${path}.actions[${i}]`);
482
+ });
244
483
  }
245
484
  walkChildren(children, path) {
246
485
  children.forEach((child, i) => this.walkNode(child, `${path}.children[${i}]`));
247
486
  }
248
487
  walkNode(node, path) {
249
488
  this.claim(node.id, path);
250
- this.checkModifiers(node, path, node.type === 'field' ? ['invisible', 'readonly', 'required'] : node.type === 'section' ? ['invisible', 'readonly'] : ['invisible']);
489
+ this.checkModifiers(node, path, node.type === 'field' ? ['invisible', 'readonly', 'required'] : node.type === 'section' || node.type === 'form' ? ['invisible', 'readonly'] : ['invisible']);
251
490
  switch (node.type) {
252
491
  case 'field':
253
492
  return this.checkFieldNode(node, path);
@@ -256,16 +495,50 @@ export class ReferenceCheck {
256
495
  this.report(path, 'a collapsible section needs a title to fold it by');
257
496
  if (node.collapsed && !node.collapsible)
258
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
+ }
259
508
  return this.walkChildren(node.children, path);
260
509
  case 'tabs':
261
510
  return this.walkTabs(node, path);
511
+ case 'form':
512
+ return this.checkFormNode(node, path);
513
+ case 'button':
514
+ return this.checkPress(node, path);
515
+ case 'text':
516
+ return this.checkValuesIn(node.text, `${path}.text`);
262
517
  default:
263
518
  return;
264
519
  }
265
520
  }
521
+ /** A saved form placed here: never this page itself, and its answers under a name no field and no other copy has. */
522
+ checkFormNode(node, path) {
523
+ // The quick check reads no shapes: a part with no page or name to go by is said here.
524
+ if (typeof node.page !== 'string' || typeof node.name !== 'string')
525
+ return this.report(path, 'a saved form placed here needs the page’s id and a name for its answers');
526
+ if (node.page === this.page.id)
527
+ this.report(`${path}.page`, 'a page cannot be placed inside itself');
528
+ const first = this.formNames.get(node.name);
529
+ if (has(this.page.fields, node.name)) {
530
+ this.report(`${path}.name`, `"${node.name}" is a field of this page: the saved form’s answers need a name of their own`);
531
+ }
532
+ else if (first !== undefined) {
533
+ this.report(`${path}.name`, `the answers of the saved form at ${first} go under "${node.name}" already: give each copy a name of its own`);
534
+ }
535
+ else
536
+ this.formNames.set(node.name, path);
537
+ }
266
538
  walkTabs(node, path) {
267
539
  node.children.forEach((tab, i) => {
268
540
  const tabPath = `${path}.children[${i}]`;
541
+ this.places.add(tab.id);
269
542
  this.claim(tab.id, tabPath);
270
543
  this.checkModifiers(tab, tabPath);
271
544
  this.walkChildren(tab.children, tabPath);
@@ -278,10 +551,22 @@ export class ReferenceCheck {
278
551
  this.need(sheet.title.subtitleField, `${path}.title.subtitleField`);
279
552
  if (sheet.title.avatarField !== undefined)
280
553
  this.need(sheet.title.avatarField, `${path}.title.avatarField`);
554
+ sheet.title.placeholderWhen?.forEach((item, i) => this.checkModifiers(item, `${path}.title.placeholderWhen[${i}]`, ['when']));
281
555
  }
282
556
  if (sheet.statusbar) {
283
- 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)');
284
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');
285
570
  if (def && def.type !== 'selection' && def.type !== 'many2one') {
286
571
  this.report(`${path}.statusbar.field`, `"${field}" is a ${def.type}; a statusbar needs a selection or many2one`);
287
572
  }
@@ -297,28 +582,66 @@ export class ReferenceCheck {
297
582
  sheet.buttons?.forEach((button, i) => {
298
583
  this.claim(button.id, `${path}.buttons[${i}]`);
299
584
  this.checkModifiers(button, `${path}.buttons[${i}]`);
585
+ this.checkPress(button, `${path}.buttons[${i}]`);
300
586
  });
301
587
  sheet.statButtons?.forEach((stat, i) => {
302
588
  this.claim(stat.id, `${path}.statButtons[${i}]`);
303
589
  this.checkModifiers(stat, `${path}.statButtons[${i}]`);
304
- if (stat.field !== undefined)
305
- this.need(stat.field, `${path}.statButtons[${i}].field`);
590
+ this.checkPress(stat, `${path}.statButtons[${i}]`);
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)');
306
598
  });
307
- if (sheet.ribbon) {
308
- this.claim(sheet.ribbon.id, `${path}.ribbon`);
309
- 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`);
310
605
  }
311
606
  sheet.badges?.forEach((badge, i) => {
312
607
  this.claim(badge.id, `${path}.badges[${i}]`);
313
608
  this.checkModifiers(badge, `${path}.badges[${i}]`);
314
609
  });
315
- for (const place of ['above', 'below']) {
610
+ for (const place of ['above', 'before', 'after', 'below']) {
316
611
  sheet.title?.[place]?.forEach((node, i) => this.walkNode(node, `${path}.title.${place}[${i}]`));
317
612
  }
318
613
  sheet.alerts?.forEach((alert, i) => {
319
- this.claim(alert.id, `${path}.alerts[${i}]`);
320
- 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`);
321
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
+ }
322
645
  this.walkChildren(sheet.children, path);
323
646
  if (sheet.sidePanel) {
324
647
  this.claim(sheet.sidePanel.id, `${path}.sidePanel`);
@@ -331,6 +654,13 @@ export class ReferenceCheck {
331
654
  node.validate?.forEach((rule, i) => {
332
655
  const at = `${path}.validate[${i}]`;
333
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
+ }
334
664
  if (rule.pattern === undefined)
335
665
  return;
336
666
  try {
@@ -340,6 +670,10 @@ export class ReferenceCheck {
340
670
  this.report(`${at}.pattern`, `"${rule.pattern}" is not a regular expression`);
341
671
  }
342
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']);
343
677
  if (def && node.totals) {
344
678
  if (def.type !== 'one2many') {
345
679
  this.report(`${path}.totals`, `totals only apply to one2many fields; "${node.field}" is a ${def.type}`);
@@ -360,16 +694,79 @@ export class ReferenceCheck {
360
694
  if (key.endsWith('Field') && typeof value === 'string')
361
695
  this.need(value, `${path}.options.${key}`);
362
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
+ }
363
715
  if (def && node.editMode && def.type !== 'one2many') {
364
716
  this.report(`${path}.editMode`, `editMode only applies to one2many fields; "${node.field}" is a ${def.type}`);
365
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
+ }
366
763
  if (def && node.optionalColumns) {
367
764
  if (def.type !== 'one2many') {
368
765
  this.report(`${path}.optionalColumns`, `optional columns only apply to one2many fields; "${node.field}" is a ${def.type}`);
369
766
  }
370
767
  else {
371
768
  // The columns the table shows: those named, or every line field but the structural ones.
372
- 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);
373
770
  for (const column of Object.keys(node.optionalColumns)) {
374
771
  if (!shown.includes(column))
375
772
  this.report(`${path}.optionalColumns.${column}`, `"${column}" is not a column of "${node.field}"`);
@@ -383,9 +780,19 @@ export class ReferenceCheck {
383
780
  if (!has(def.fields, column)) {
384
781
  this.report(`${path}.columns[${i}]`, `"${column}" is not a field of the lines of "${node.field}"`);
385
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}"`);
386
793
  });
387
794
  }
388
- else if (def.type !== 'many2many') {
795
+ else {
389
796
  this.report(`${path}.columns`, `columns only apply to one2many and many2many fields; "${node.field}" is a ${def.type}`);
390
797
  }
391
798
  }