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