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