@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,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,16 +19,151 @@ 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
- constructor(page) {
25
+ /** Where the answers of each saved form placed here go, by name: the path of the part that took it first. */
26
+ formNames = new Map();
27
+ /** The ids of the tabs and wizard steps a step can go to, and where each goTo and show names one. */
28
+ places = new Set();
29
+ goingTo = [];
30
+ constructor(page, line) {
22
31
  this.page = page;
32
+ this.line = line;
23
33
  }
24
34
  run() {
25
- this.checkFields(this.page.fields, 'fields');
35
+ this.checkFields(this.page.fields, 'fields', this.line?.name, this.line?.parent);
26
36
  this.walkRoot(this.page.layout, 'layout');
37
+ if (this.page.on)
38
+ this.checkEvents(this.page.on, 'on');
39
+ this.checkGoingTo();
27
40
  return this.issues;
28
41
  }
42
+ /** Each step's goTo and show names a tab or a wizard step of this page. */
43
+ checkGoingTo() {
44
+ for (const { target, path } of this.goingTo) {
45
+ if (!this.places.has(target))
46
+ this.report(path, `no tab or wizard step "${target}" on this page`);
47
+ }
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);
63
+ }
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. */
65
+ checkEvents(on, path) {
66
+ for (const moment of ['open', 'beforeSave', 'afterSave']) {
67
+ const steps = on[moment];
68
+ if (steps)
69
+ this.checkSteps(steps, `${path}.${moment}`);
70
+ }
71
+ for (const [field, steps] of Object.entries(on.change ?? {})) {
72
+ if (!this.formNames.has(field))
73
+ this.need(field, `${path}.change.${field}`);
74
+ this.checkSteps(steps, `${path}.change.${field}`);
75
+ }
76
+ for (const [target, steps] of Object.entries(on.show ?? {})) {
77
+ this.goingTo.push({ target, path: `${path}.show.${target}` });
78
+ this.checkSteps(steps, `${path}.show.${target}`);
79
+ }
80
+ }
81
+ /** A press: steps, an app action, or both — and its steps checked. */
82
+ checkPress(button, path) {
83
+ if (!button.steps && button.action === undefined)
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`);
88
+ if (button.steps)
89
+ this.checkSteps(button.steps, `${path}.steps`);
90
+ }
91
+ /**
92
+ * Steps, each against this page: the fields it sets, empties, checks or adds
93
+ * a line to are this page's; its expressions read; a step it goes to is a
94
+ * tab or a wizard step here. The opened page's own fields are not known
95
+ * here: what it starts with is named freely, and what comes back is read
96
+ * as an expression.
97
+ */
98
+ checkSteps(steps, path) {
99
+ const scope = this.here();
100
+ steps.forEach((step, i) => {
101
+ const at = `${path}[${i}]`;
102
+ this.checkModifiers(step, at, ['when']);
103
+ switch (step.do) {
104
+ case 'set': {
105
+ const def = this.need(step.field, `${at}.field`);
106
+ if (def && !EXPRESSIBLE.has(def.type))
107
+ this.report(`${at}.field`, `a ${def.type} cannot be set from an expression; ${EXPRESSIBLE_WORDS}`);
108
+ this.checkExpression(step.value, `${at}.value`, scope);
109
+ return;
110
+ }
111
+ case 'clear':
112
+ this.need(step.field, `${at}.field`);
113
+ return;
114
+ case 'addLine': {
115
+ const def = this.need(step.field, `${at}.field`);
116
+ if (def && def.type !== 'one2many')
117
+ return this.report(`${at}.field`, `"${step.field}" is a ${def.type}; a line is added to a one2many`);
118
+ for (const [name, value] of Object.entries(step.values ?? {})) {
119
+ if (def?.type === 'one2many' && !has(def.fields, name))
120
+ this.report(`${at}.values.${name}`, `"${name}" is not a field of the lines of "${step.field}"`);
121
+ this.checkExpression(value, `${at}.values.${name}`, scope);
122
+ }
123
+ return;
124
+ }
125
+ case 'check':
126
+ step.fields?.forEach((field, j) => this.need(field, `${at}.fields[${j}]`));
127
+ return;
128
+ case 'goTo':
129
+ this.goingTo.push({ target: step.target, path: `${at}.target` });
130
+ return;
131
+ case 'open':
132
+ if (step.page === this.page.id && step.as === 'page')
133
+ this.report(`${at}.page`, 'a page cannot open itself in its own place');
134
+ if (step.side !== undefined && step.as !== 'panel')
135
+ this.report(`${at}.side`, `a side is where a panel comes from; this opens ${step.as === 'page' ? 'in its place' : 'in a dialog'}`);
136
+ if (step.record !== undefined)
137
+ this.checkExpression(step.record, `${at}.record`, scope);
138
+ for (const [name, value] of Object.entries(step.values ?? {}))
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));
142
+ for (const [name, value] of Object.entries(step.into ?? {})) {
143
+ this.need(name, `${at}.into.${name}`);
144
+ try {
145
+ (0, expression_1.compileExpression)(value);
146
+ }
147
+ catch (error) {
148
+ this.report(`${at}.into.${name}`, `cannot read "${value}": ${error.message}`);
149
+ }
150
+ }
151
+ if (step.then)
152
+ this.checkSteps(step.then, `${at}.then`);
153
+ return;
154
+ case 'openUrl':
155
+ this.checkExpression(step.url, `${at}.url`, scope);
156
+ return;
157
+ default:
158
+ return;
159
+ }
160
+ });
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
+ }
29
167
  report(path, message) {
30
168
  this.issues.push({ path, message });
31
169
  }
@@ -43,28 +181,94 @@ class ReferenceCheck {
43
181
  this.report(path, `no field "${name}"`);
44
182
  return undefined;
45
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
+ }
46
215
  /** Every modifier on `owner` must read, and read only fields of this page (or of its line). */
47
- checkModifiers(owner, path, keys = ['invisible'], scope = { fields: this.page.fields }) {
216
+ checkModifiers(owner, path, keys = ['invisible'], scope = this.here()) {
48
217
  for (const key of keys) {
49
218
  const value = owner[key];
50
219
  if (typeof value !== 'string')
51
220
  continue;
52
- let fields;
221
+ let compiled;
53
222
  try {
54
- fields = (0, modifier_1.compileModifier)(value).fields;
223
+ compiled = (0, modifier_1.compileModifier)(value);
55
224
  }
56
225
  catch (error) {
57
226
  this.report(`${path}.${key}`, `cannot read "${value}": ${error.message}`);
58
227
  continue;
59
228
  }
60
- this.checkReads(value, fields, `${path}.${key}`, scope);
229
+ this.checkReads(value, compiled, `${path}.${key}`, scope);
61
230
  }
62
231
  }
63
- /** Each name an expression reads must be a field where it is worked out. */
64
- checkReads(source, fields, path, scope) {
65
- 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) {
66
256
  if (has(scope.fields, name))
67
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
+ }
68
272
  const where = scope.lines === undefined ? 'of this page' : `of the lines of "${scope.lines}"`;
69
273
  this.report(path, `"${source}" reads "${name}", which is not a field ${where}`);
70
274
  }
@@ -83,7 +287,7 @@ class ReferenceCheck {
83
287
  this.report(path, `cannot read "${source}": ${error.message}`);
84
288
  return null;
85
289
  }
86
- this.checkReads(source, compiled.fields, path, scope);
290
+ this.checkReads(source, compiled, path, scope);
87
291
  for (const sum of compiled.sums) {
88
292
  if (!has(scope.fields, sum.lines))
89
293
  continue;
@@ -117,6 +321,7 @@ class ReferenceCheck {
117
321
  }
118
322
  def.setWhen?.forEach((item, i) => {
119
323
  this.checkModifiers(item, `${path}.setWhen[${i}]`, ['when'], scope);
324
+ item.on?.forEach((other, j) => this.need(other, `${path}.setWhen[${i}].on[${j}]`, fields));
120
325
  this.checkExpression(item.value, `${path}.setWhen[${i}].value`, scope);
121
326
  });
122
327
  }
@@ -126,32 +331,36 @@ class ReferenceCheck {
126
331
  this.report(`${base}.${cycle[0]}.compute`, message);
127
332
  }
128
333
  }
129
- checkFields(fields, base, lines) {
130
- 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);
131
337
  for (const [name, def] of Object.entries(fields)) {
132
338
  const path = `${base}.${name}`;
133
- if (def.type === 'monetary' && def.currencyField !== undefined) {
134
- const currency = this.need(def.currencyField, `${path}.currencyField`, fields);
135
- if (currency && !['many2one', 'selection', 'char'].includes(currency.type)) {
136
- 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);
137
355
  }
138
356
  }
139
357
  if ((def.type === 'many2one' || def.type === 'many2many') && def.filter) {
140
- // Groups nest: each valueFrom inside them must name a field too.
141
- const walk = (item, at) => {
142
- if ('any' in item)
143
- return item.any.forEach((inner, i) => walk(inner, `${at}.any[${i}]`));
144
- if ('all' in item)
145
- return item.all.forEach((inner, i) => walk(inner, `${at}.all[${i}]`));
146
- if (item.valueFrom !== undefined)
147
- this.need(item.valueFrom, `${at}.valueFrom`, fields);
148
- };
149
- def.filter.forEach((item, i) => {
150
- walk(item, `${path}.filter[${i}]`);
151
- });
358
+ def.filter.forEach((item, i) => this.checkFilterItem(item, `${path}.filter[${i}]`, scope));
152
359
  }
153
360
  if (def.type === 'selection')
154
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));
155
364
  if (def.type === 'properties' && def.definitions) {
156
365
  const seen = new Set();
157
366
  def.definitions.forEach((property, i) => {
@@ -164,8 +373,9 @@ class ReferenceCheck {
164
373
  });
165
374
  }
166
375
  if (def.type === 'one2many') {
167
- this.checkFields(def.fields, `${path}.fields`, name);
168
- 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)
169
379
  this.checkLineKinds(name, def.lineKinds, def.fields, `${path}.lineKinds`);
170
380
  if (def.sequenceField !== undefined) {
171
381
  const sequence = def.fields[def.sequenceField];
@@ -178,6 +388,21 @@ class ReferenceCheck {
178
388
  }
179
389
  }
180
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
+ }
181
406
  checkLineKinds(field, kinds, lineFields, path) {
182
407
  const kindField = lineFields[kinds.field];
183
408
  if (!kindField)
@@ -199,10 +424,20 @@ class ReferenceCheck {
199
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`);
200
425
  }
201
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
+ }
202
435
  walkRoot(root, path) {
203
436
  if (root.type === 'tabs')
204
437
  return this.walkNode(root, path);
205
438
  this.claim(root.id, path);
439
+ if (root.type === 'sheet' || root.type === 'sections')
440
+ this.walkFooter(root.footer, path);
206
441
  if (root.type === 'sheet')
207
442
  return this.walkSheet(root, path);
208
443
  if (root.type === 'sections')
@@ -211,6 +446,7 @@ class ReferenceCheck {
211
446
  return this.walkList(root, path);
212
447
  root.children.forEach((step, i) => {
213
448
  const stepPath = `${path}.children[${i}]`;
449
+ this.places.add(step.id);
214
450
  this.claim(step.id, stepPath);
215
451
  this.checkModifiers(step, stepPath);
216
452
  this.walkChildren(step.children, stepPath);
@@ -243,14 +479,17 @@ class ReferenceCheck {
243
479
  if (!named.has(id))
244
480
  this.report(`${path}.defaultFilters[${i}]`, `no filter "${id}" in this list`);
245
481
  });
246
- list.actions?.forEach((button, i) => this.claim(button.id, `${path}.actions[${i}]`));
482
+ list.actions?.forEach((button, i) => {
483
+ this.claim(button.id, `${path}.actions[${i}]`);
484
+ this.checkPress(button, `${path}.actions[${i}]`);
485
+ });
247
486
  }
248
487
  walkChildren(children, path) {
249
488
  children.forEach((child, i) => this.walkNode(child, `${path}.children[${i}]`));
250
489
  }
251
490
  walkNode(node, path) {
252
491
  this.claim(node.id, path);
253
- this.checkModifiers(node, path, node.type === 'field' ? ['invisible', 'readonly', 'required'] : node.type === 'section' ? ['invisible', 'readonly'] : ['invisible']);
492
+ this.checkModifiers(node, path, node.type === 'field' ? ['invisible', 'readonly', 'required'] : node.type === 'section' || node.type === 'form' ? ['invisible', 'readonly'] : ['invisible']);
254
493
  switch (node.type) {
255
494
  case 'field':
256
495
  return this.checkFieldNode(node, path);
@@ -259,16 +498,50 @@ class ReferenceCheck {
259
498
  this.report(path, 'a collapsible section needs a title to fold it by');
260
499
  if (node.collapsed && !node.collapsible)
261
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
+ }
262
511
  return this.walkChildren(node.children, path);
263
512
  case 'tabs':
264
513
  return this.walkTabs(node, path);
514
+ case 'form':
515
+ return this.checkFormNode(node, path);
516
+ case 'button':
517
+ return this.checkPress(node, path);
518
+ case 'text':
519
+ return this.checkValuesIn(node.text, `${path}.text`);
265
520
  default:
266
521
  return;
267
522
  }
268
523
  }
524
+ /** A saved form placed here: never this page itself, and its answers under a name no field and no other copy has. */
525
+ checkFormNode(node, path) {
526
+ // The quick check reads no shapes: a part with no page or name to go by is said here.
527
+ if (typeof node.page !== 'string' || typeof node.name !== 'string')
528
+ return this.report(path, 'a saved form placed here needs the page’s id and a name for its answers');
529
+ if (node.page === this.page.id)
530
+ this.report(`${path}.page`, 'a page cannot be placed inside itself');
531
+ const first = this.formNames.get(node.name);
532
+ if (has(this.page.fields, node.name)) {
533
+ this.report(`${path}.name`, `"${node.name}" is a field of this page: the saved form’s answers need a name of their own`);
534
+ }
535
+ else if (first !== undefined) {
536
+ 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`);
537
+ }
538
+ else
539
+ this.formNames.set(node.name, path);
540
+ }
269
541
  walkTabs(node, path) {
270
542
  node.children.forEach((tab, i) => {
271
543
  const tabPath = `${path}.children[${i}]`;
544
+ this.places.add(tab.id);
272
545
  this.claim(tab.id, tabPath);
273
546
  this.checkModifiers(tab, tabPath);
274
547
  this.walkChildren(tab.children, tabPath);
@@ -281,10 +554,22 @@ class ReferenceCheck {
281
554
  this.need(sheet.title.subtitleField, `${path}.title.subtitleField`);
282
555
  if (sheet.title.avatarField !== undefined)
283
556
  this.need(sheet.title.avatarField, `${path}.title.avatarField`);
557
+ sheet.title.placeholderWhen?.forEach((item, i) => this.checkModifiers(item, `${path}.title.placeholderWhen[${i}]`, ['when']));
284
558
  }
285
559
  if (sheet.statusbar) {
286
- 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)');
287
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');
288
573
  if (def && def.type !== 'selection' && def.type !== 'many2one') {
289
574
  this.report(`${path}.statusbar.field`, `"${field}" is a ${def.type}; a statusbar needs a selection or many2one`);
290
575
  }
@@ -300,28 +585,66 @@ class ReferenceCheck {
300
585
  sheet.buttons?.forEach((button, i) => {
301
586
  this.claim(button.id, `${path}.buttons[${i}]`);
302
587
  this.checkModifiers(button, `${path}.buttons[${i}]`);
588
+ this.checkPress(button, `${path}.buttons[${i}]`);
303
589
  });
304
590
  sheet.statButtons?.forEach((stat, i) => {
305
591
  this.claim(stat.id, `${path}.statButtons[${i}]`);
306
592
  this.checkModifiers(stat, `${path}.statButtons[${i}]`);
307
- if (stat.field !== undefined)
308
- this.need(stat.field, `${path}.statButtons[${i}].field`);
593
+ this.checkPress(stat, `${path}.statButtons[${i}]`);
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)');
309
601
  });
310
- if (sheet.ribbon) {
311
- this.claim(sheet.ribbon.id, `${path}.ribbon`);
312
- 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`);
313
608
  }
314
609
  sheet.badges?.forEach((badge, i) => {
315
610
  this.claim(badge.id, `${path}.badges[${i}]`);
316
611
  this.checkModifiers(badge, `${path}.badges[${i}]`);
317
612
  });
318
- for (const place of ['above', 'below']) {
613
+ for (const place of ['above', 'before', 'after', 'below']) {
319
614
  sheet.title?.[place]?.forEach((node, i) => this.walkNode(node, `${path}.title.${place}[${i}]`));
320
615
  }
321
616
  sheet.alerts?.forEach((alert, i) => {
322
- this.claim(alert.id, `${path}.alerts[${i}]`);
323
- 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`);
324
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
+ }
325
648
  this.walkChildren(sheet.children, path);
326
649
  if (sheet.sidePanel) {
327
650
  this.claim(sheet.sidePanel.id, `${path}.sidePanel`);
@@ -334,6 +657,13 @@ class ReferenceCheck {
334
657
  node.validate?.forEach((rule, i) => {
335
658
  const at = `${path}.validate[${i}]`;
336
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
+ }
337
667
  if (rule.pattern === undefined)
338
668
  return;
339
669
  try {
@@ -343,6 +673,10 @@ class ReferenceCheck {
343
673
  this.report(`${at}.pattern`, `"${rule.pattern}" is not a regular expression`);
344
674
  }
345
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']);
346
680
  if (def && node.totals) {
347
681
  if (def.type !== 'one2many') {
348
682
  this.report(`${path}.totals`, `totals only apply to one2many fields; "${node.field}" is a ${def.type}`);
@@ -363,16 +697,79 @@ class ReferenceCheck {
363
697
  if (key.endsWith('Field') && typeof value === 'string')
364
698
  this.need(value, `${path}.options.${key}`);
365
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
+ }
366
718
  if (def && node.editMode && def.type !== 'one2many') {
367
719
  this.report(`${path}.editMode`, `editMode only applies to one2many fields; "${node.field}" is a ${def.type}`);
368
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
+ }
369
766
  if (def && node.optionalColumns) {
370
767
  if (def.type !== 'one2many') {
371
768
  this.report(`${path}.optionalColumns`, `optional columns only apply to one2many fields; "${node.field}" is a ${def.type}`);
372
769
  }
373
770
  else {
374
771
  // The columns the table shows: those named, or every line field but the structural ones.
375
- 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);
376
773
  for (const column of Object.keys(node.optionalColumns)) {
377
774
  if (!shown.includes(column))
378
775
  this.report(`${path}.optionalColumns.${column}`, `"${column}" is not a column of "${node.field}"`);
@@ -386,9 +783,19 @@ class ReferenceCheck {
386
783
  if (!has(def.fields, column)) {
387
784
  this.report(`${path}.columns[${i}]`, `"${column}" is not a field of the lines of "${node.field}"`);
388
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}"`);
389
796
  });
390
797
  }
391
- else if (def.type !== 'many2many') {
798
+ else {
392
799
  this.report(`${path}.columns`, `columns only apply to one2many and many2many fields; "${node.field}" is a ${def.type}`);
393
800
  }
394
801
  }