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