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