@fieldia/core 0.23.0 → 0.25.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 +211 -3
- package/package.json +1 -1
- package/schema/page.schema.json +1051 -325
- package/src/index.d.mts +9 -2
- package/src/index.d.ts +9 -2
- package/src/index.js +31 -1
- package/src/index.js.map +1 -1
- package/src/index.mjs +12 -2
- package/src/index.mjs.map +1 -1
- package/src/lib/format/actions.d.mts +31 -1
- package/src/lib/format/actions.d.ts +31 -1
- package/src/lib/format/actions.js +1 -0
- package/src/lib/format/actions.js.map +1 -1
- package/src/lib/format/actions.mjs +1 -0
- package/src/lib/format/actions.mjs.map +1 -1
- package/src/lib/format/autofill-group.d.mts +12 -0
- package/src/lib/format/autofill-group.d.ts +12 -0
- package/src/lib/format/autofill-group.js +23 -0
- package/src/lib/format/autofill-group.js.map +1 -0
- package/src/lib/format/autofill-group.mjs +19 -0
- package/src/lib/format/autofill-group.mjs.map +1 -0
- package/src/lib/format/autofill.d.mts +14 -0
- package/src/lib/format/autofill.d.ts +14 -0
- package/src/lib/format/autofill.js +54 -0
- package/src/lib/format/autofill.js.map +1 -0
- package/src/lib/format/autofill.mjs +48 -0
- package/src/lib/format/autofill.mjs.map +1 -0
- package/src/lib/format/bulk-edit.d.mts +18 -0
- package/src/lib/format/bulk-edit.d.ts +18 -0
- package/src/lib/format/bulk-edit.js +26 -0
- package/src/lib/format/bulk-edit.js.map +1 -0
- package/src/lib/format/bulk-edit.mjs +21 -0
- package/src/lib/format/bulk-edit.mjs.map +1 -0
- package/src/lib/format/duplicate-kinds.d.mts +10 -0
- package/src/lib/format/duplicate-kinds.d.ts +10 -0
- package/src/lib/format/duplicate-kinds.js +14 -0
- package/src/lib/format/duplicate-kinds.js.map +1 -0
- package/src/lib/format/duplicate-kinds.mjs +10 -0
- package/src/lib/format/duplicate-kinds.mjs.map +1 -0
- package/src/lib/format/duplicates.d.mts +22 -0
- package/src/lib/format/duplicates.d.ts +22 -0
- package/src/lib/format/duplicates.js +24 -0
- package/src/lib/format/duplicates.js.map +1 -0
- package/src/lib/format/duplicates.mjs +21 -0
- package/src/lib/format/duplicates.mjs.map +1 -0
- package/src/lib/format/field.d.mts +291 -34
- package/src/lib/format/field.d.ts +291 -34
- package/src/lib/format/field.js +19 -4
- package/src/lib/format/field.js.map +1 -1
- package/src/lib/format/field.mjs +17 -2
- package/src/lib/format/field.mjs.map +1 -1
- package/src/lib/format/import-fields.d.mts +18 -0
- package/src/lib/format/import-fields.d.ts +18 -0
- package/src/lib/format/import-fields.js +26 -0
- package/src/lib/format/import-fields.js.map +1 -0
- package/src/lib/format/import-fields.mjs +21 -0
- package/src/lib/format/import-fields.mjs.map +1 -0
- package/src/lib/format/layout.d.mts +7357 -3127
- package/src/lib/format/layout.d.ts +7357 -3127
- package/src/lib/format/layout.js +108 -32
- package/src/lib/format/layout.js.map +1 -1
- package/src/lib/format/layout.mjs +107 -32
- package/src/lib/format/layout.mjs.map +1 -1
- package/src/lib/format/layout.types.d.mts +5 -2
- package/src/lib/format/layout.types.d.ts +5 -2
- package/src/lib/format/page.d.mts +1080 -106
- package/src/lib/format/page.d.ts +1080 -106
- package/src/lib/format/page.js +25 -0
- package/src/lib/format/page.js.map +1 -1
- package/src/lib/format/page.mjs +26 -1
- package/src/lib/format/page.mjs.map +1 -1
- package/src/lib/format/phone.d.mts +27 -0
- package/src/lib/format/phone.d.ts +27 -0
- package/src/lib/format/phone.js +43 -0
- package/src/lib/format/phone.js.map +1 -0
- package/src/lib/format/phone.mjs +39 -0
- package/src/lib/format/phone.mjs.map +1 -0
- package/src/lib/format/references.d.mts +39 -0
- package/src/lib/format/references.d.ts +39 -0
- package/src/lib/format/references.js +263 -20
- package/src/lib/format/references.js.map +1 -1
- package/src/lib/format/references.mjs +263 -20
- package/src/lib/format/references.mjs.map +1 -1
- package/src/lib/format/slots-options.d.mts +26 -0
- package/src/lib/format/slots-options.d.ts +26 -0
- package/src/lib/format/slots-options.js +35 -0
- package/src/lib/format/slots-options.js.map +1 -0
- package/src/lib/format/slots-options.mjs +31 -0
- package/src/lib/format/slots-options.mjs.map +1 -0
- package/src/lib/format/translate.js +8 -4
- package/src/lib/format/translate.js.map +1 -1
- package/src/lib/format/translate.mjs +8 -4
- package/src/lib/format/translate.mjs.map +1 -1
- package/src/lib/format/upgrade.d.mts +8 -3
- package/src/lib/format/upgrade.d.ts +8 -3
- package/src/lib/format/upgrade.js +8 -3
- package/src/lib/format/upgrade.js.map +1 -1
- package/src/lib/format/upgrade.mjs +8 -3
- package/src/lib/format/upgrade.mjs.map +1 -1
- package/src/lib/format/version.d.mts +2 -2
- package/src/lib/format/version.d.ts +2 -2
- package/src/lib/format/version.js +2 -2
- package/src/lib/format/version.js.map +1 -1
- package/src/lib/format/version.mjs +2 -2
- package/src/lib/format/version.mjs.map +1 -1
- package/src/lib/format/zoned.d.mts +8 -0
- package/src/lib/format/zoned.d.ts +8 -0
- package/src/lib/format/zoned.js +36 -0
- package/src/lib/format/zoned.js.map +1 -0
- package/src/lib/format/zoned.mjs +33 -0
- package/src/lib/format/zoned.mjs.map +1 -0
- package/src/lib/record/check.d.mts +6 -1
- package/src/lib/record/check.d.ts +6 -1
- package/src/lib/record/check.js +12 -4
- package/src/lib/record/check.js.map +1 -1
- package/src/lib/record/check.mjs +12 -4
- package/src/lib/record/check.mjs.map +1 -1
- package/src/lib/record/data-source.d.mts +309 -3
- package/src/lib/record/data-source.d.ts +309 -3
- package/src/lib/record/data-source.js +16 -0
- package/src/lib/record/data-source.js.map +1 -1
- package/src/lib/record/data-source.mjs +15 -0
- package/src/lib/record/data-source.mjs.map +1 -1
- package/src/lib/record/form.d.mts +116 -25
- package/src/lib/record/form.d.ts +116 -25
- package/src/lib/record/form.js +457 -78
- package/src/lib/record/form.js.map +1 -1
- package/src/lib/record/form.mjs +457 -78
- package/src/lib/record/form.mjs.map +1 -1
- package/src/lib/record/input-rules.d.mts +51 -9
- package/src/lib/record/input-rules.d.ts +51 -9
- package/src/lib/record/input-rules.js +119 -20
- package/src/lib/record/input-rules.js.map +1 -1
- package/src/lib/record/input-rules.mjs +118 -19
- package/src/lib/record/input-rules.mjs.map +1 -1
- package/src/lib/record/inputs.js +4 -2
- package/src/lib/record/inputs.js.map +1 -1
- package/src/lib/record/inputs.mjs +4 -2
- package/src/lib/record/inputs.mjs.map +1 -1
- package/src/lib/record/limits.d.mts +1 -1
- package/src/lib/record/limits.d.ts +1 -1
- package/src/lib/record/limits.js +4 -2
- package/src/lib/record/limits.js.map +1 -1
- package/src/lib/record/limits.mjs +4 -2
- package/src/lib/record/limits.mjs.map +1 -1
- package/src/lib/record/locales/ar.js +3 -2
- package/src/lib/record/locales/ar.js.map +1 -1
- package/src/lib/record/locales/ar.mjs +3 -2
- package/src/lib/record/locales/ar.mjs.map +1 -1
- package/src/lib/record/locales/de.js +3 -2
- package/src/lib/record/locales/de.js.map +1 -1
- package/src/lib/record/locales/de.mjs +3 -2
- package/src/lib/record/locales/de.mjs.map +1 -1
- package/src/lib/record/locales/fr.js +3 -2
- package/src/lib/record/locales/fr.js.map +1 -1
- package/src/lib/record/locales/fr.mjs +3 -2
- package/src/lib/record/locales/fr.mjs.map +1 -1
- package/src/lib/record/memory-data-source.d.mts +20 -2
- package/src/lib/record/memory-data-source.d.ts +20 -2
- package/src/lib/record/memory-data-source.js +153 -35
- package/src/lib/record/memory-data-source.js.map +1 -1
- package/src/lib/record/memory-data-source.mjs +154 -36
- package/src/lib/record/memory-data-source.mjs.map +1 -1
- package/src/lib/record/memory-duplicates.d.mts +19 -0
- package/src/lib/record/memory-duplicates.d.ts +19 -0
- package/src/lib/record/memory-duplicates.js +78 -0
- package/src/lib/record/memory-duplicates.js.map +1 -0
- package/src/lib/record/memory-duplicates.mjs +74 -0
- package/src/lib/record/memory-duplicates.mjs.map +1 -0
- package/src/lib/record/memory-schedules.d.mts +45 -0
- package/src/lib/record/memory-schedules.d.ts +45 -0
- package/src/lib/record/memory-schedules.js +81 -0
- package/src/lib/record/memory-schedules.js.map +1 -0
- package/src/lib/record/memory-schedules.mjs +78 -0
- package/src/lib/record/memory-schedules.mjs.map +1 -0
- package/src/lib/record/messages.d.mts +3 -1
- package/src/lib/record/messages.d.ts +3 -1
- package/src/lib/record/messages.js +3 -2
- package/src/lib/record/messages.js.map +1 -1
- package/src/lib/record/messages.mjs +3 -2
- package/src/lib/record/messages.mjs.map +1 -1
- package/src/lib/record/periods.d.mts +47 -0
- package/src/lib/record/periods.d.ts +47 -0
- package/src/lib/record/periods.js +86 -0
- package/src/lib/record/periods.js.map +1 -0
- package/src/lib/record/periods.mjs +79 -0
- package/src/lib/record/periods.mjs.map +1 -0
- package/src/lib/record/rules.js +5 -6
- package/src/lib/record/rules.js.map +1 -1
- package/src/lib/record/rules.mjs +5 -6
- package/src/lib/record/rules.mjs.map +1 -1
- package/src/lib/record/run.d.mts +75 -9
- package/src/lib/record/run.d.ts +75 -9
- package/src/lib/record/run.js +169 -26
- package/src/lib/record/run.js.map +1 -1
- package/src/lib/record/run.mjs +170 -27
- package/src/lib/record/run.mjs.map +1 -1
- package/src/lib/record/search.d.mts +13 -3
- package/src/lib/record/search.d.ts +13 -3
- package/src/lib/record/search.js +11 -4
- package/src/lib/record/search.js.map +1 -1
- package/src/lib/record/search.mjs +11 -4
- package/src/lib/record/search.mjs.map +1 -1
- package/src/lib/record/waits.d.mts +32 -0
- package/src/lib/record/waits.d.ts +32 -0
- package/src/lib/record/waits.js +89 -0
- package/src/lib/record/waits.js.map +1 -0
- package/src/lib/record/waits.mjs +85 -0
- package/src/lib/record/waits.mjs.map +1 -0
- package/src/lib/record/zones.d.mts +57 -0
- package/src/lib/record/zones.d.ts +57 -0
- package/src/lib/record/zones.js +147 -0
- package/src/lib/record/zones.js.map +1 -0
- package/src/lib/record/zones.mjs +132 -0
- package/src/lib/record/zones.mjs.map +1 -0
- package/src/runtime.d.mts +19 -9
- package/src/runtime.d.ts +19 -9
- package/src/runtime.js +37 -2
- package/src/runtime.js.map +1 -1
- package/src/runtime.mjs +9 -2
- package/src/runtime.mjs.map +1 -1
|
@@ -7,13 +7,20 @@ import { BROWSER_HOTKEYS, hotkeyWords } from './hotkeys.mjs';
|
|
|
7
7
|
import { checkButtonParts, checkHead } from './head-checks.mjs';
|
|
8
8
|
import { checkSectionTracks } from './section-checks.mjs';
|
|
9
9
|
import { fieldPattern } from './pattern.mjs';
|
|
10
|
+
import { BULK_KINDS, bulkSettable } from './bulk-edit.mjs';
|
|
11
|
+
import { DUPLICATE_KINDS } from './duplicate-kinds.mjs';
|
|
12
|
+
import { IMPORT_KINDS, importable } from './import-fields.mjs';
|
|
13
|
+
import { isTimeZone } from '../record/zones.mjs';
|
|
10
14
|
import { fill, MESSAGES } from '../record/messages.mjs';
|
|
15
|
+
import { autofillGroupProblem } from './autofill-group.mjs';
|
|
11
16
|
const has = (record, key) => Object.prototype.hasOwnProperty.call(record, key);
|
|
12
17
|
/** The kinds of field an expression can give a value to: the rest hold records, lines or files. */
|
|
13
18
|
const EXPRESSIBLE = new Set(['char', 'text', 'html', 'integer', 'float', 'monetary', 'boolean', 'date', 'datetime', 'selection', 'json']);
|
|
14
19
|
const EXPRESSIBLE_WORDS = 'text, numbers, yes or no, dates, choices and json can';
|
|
15
20
|
/** The kinds of field a list's filter bar can list the values of. */
|
|
16
21
|
const LISTS_ITS_VALUES = new Set(['selection', 'many2one', 'many2many', 'boolean', 'char', 'integer']);
|
|
22
|
+
/** Kinds of field a list's card shows as its picture: a picture held, or text holding a picture's address. */
|
|
23
|
+
const PICTURES = new Set(['image', 'binary', 'char']);
|
|
17
24
|
/** What a calendar's start and end can be: a day, or a day and a time. */
|
|
18
25
|
const CALENDAR_TIMES = new Set(['date', 'datetime']);
|
|
19
26
|
/** What a record's words on a calendar can be. */
|
|
@@ -38,11 +45,15 @@ export class ReferenceCheck {
|
|
|
38
45
|
/** The ids of the tabs and wizard steps a step can go to, and where each goTo and show names one. */
|
|
39
46
|
places = new Set();
|
|
40
47
|
goingTo = [];
|
|
48
|
+
/** The names of the slots the layout places. */
|
|
49
|
+
slots = new Set();
|
|
41
50
|
constructor(page, line) {
|
|
42
51
|
this.page = page;
|
|
43
52
|
this.line = line;
|
|
44
53
|
}
|
|
45
54
|
run() {
|
|
55
|
+
if (this.page.timeZone !== undefined)
|
|
56
|
+
this.checkZone(this.page.timeZone, 'timeZone');
|
|
46
57
|
this.checkFields(this.page.fields, 'fields', this.line?.name, this.line?.parent);
|
|
47
58
|
this.walkRoot(this.page.layout, 'layout');
|
|
48
59
|
checkHead(this.page.layout, 'layout', {
|
|
@@ -58,9 +69,46 @@ export class ReferenceCheck {
|
|
|
58
69
|
});
|
|
59
70
|
if (this.page.on)
|
|
60
71
|
this.checkEvents(this.page.on, 'on');
|
|
72
|
+
if (this.page.botCheck !== undefined && !this.line)
|
|
73
|
+
this.checkBotCheck(this.page);
|
|
74
|
+
if (this.page.duplicates !== undefined && !this.line)
|
|
75
|
+
this.checkDuplicates(this.page);
|
|
61
76
|
this.checkGoingTo();
|
|
62
77
|
return this.issues;
|
|
63
78
|
}
|
|
79
|
+
/** A bot check: on a page of responses, its captcha drawn in a slot the page places. */
|
|
80
|
+
checkBotCheck(page) {
|
|
81
|
+
const check = page.botCheck;
|
|
82
|
+
if (page.data.kind !== 'responses')
|
|
83
|
+
return this.report('botCheck', 'a bot check is for a page of responses that anyone may send; a record’s page is behind the app’s own sign-in');
|
|
84
|
+
const captcha = check?.captcha;
|
|
85
|
+
if (typeof captcha === 'string' && !this.slots.has(captcha))
|
|
86
|
+
this.report('botCheck.captcha', `no slot "${captcha}" on this page to draw the captcha in: place { "type": "slot", "id": "${captcha}", "name": "${captcha}" } where it goes`);
|
|
87
|
+
}
|
|
88
|
+
/** Possible duplicates: asked of a record's own page, by fields records can be compared by, each named once. */
|
|
89
|
+
checkDuplicates(page) {
|
|
90
|
+
const given = page.duplicates?.fields;
|
|
91
|
+
const named = Array.isArray(given) ? given : [];
|
|
92
|
+
if (!named.length)
|
|
93
|
+
this.report('duplicates.fields', 'name the fields records are compared by: ["phone", "email"]');
|
|
94
|
+
if (page.data.kind !== 'record')
|
|
95
|
+
this.report('duplicates', 'duplicates are asked of records like the one typed: a page of responses keeps none to compare it with');
|
|
96
|
+
else if (page.layout.type === 'list')
|
|
97
|
+
this.report('duplicates', 'duplicates are asked as a record is typed, on its own page, not on a list of records');
|
|
98
|
+
named.forEach((name, i) => {
|
|
99
|
+
const at = `duplicates.fields[${i}]`;
|
|
100
|
+
if (named.indexOf(name) !== i)
|
|
101
|
+
return this.report(at, `"${name}" is named twice`);
|
|
102
|
+
const def = this.need(name, at);
|
|
103
|
+
if (def && !DUPLICATE_KINDS.has(def.type))
|
|
104
|
+
this.report(at, `"${name}" is a ${def.type}; records are compared by words, a number, a date, a choice or a link`);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** A time zone the browser knows, by its IANA name: a page's dates and times are never drawn on a clock it cannot read. */
|
|
108
|
+
checkZone(zone, path) {
|
|
109
|
+
if (!isTimeZone(zone))
|
|
110
|
+
this.report(path, `${JSON.stringify(zone)} is not a time zone this browser knows: name one as the IANA does, such as "Africa/Cairo"`);
|
|
111
|
+
}
|
|
64
112
|
/** Each step's goTo and show names a tab or a wizard step of this page. */
|
|
65
113
|
checkGoingTo() {
|
|
66
114
|
for (const { target, path } of this.goingTo) {
|
|
@@ -392,6 +440,14 @@ export class ReferenceCheck {
|
|
|
392
440
|
if (pattern instanceof Error)
|
|
393
441
|
this.report(`${path}.pattern`, fill(MESSAGES.en.patternBroken, { label: def.label, problem: pattern.message }));
|
|
394
442
|
}
|
|
443
|
+
if (def.type === 'datetime' && def.timeZone !== undefined)
|
|
444
|
+
this.checkZone(def.timeZone, `${path}.timeZone`);
|
|
445
|
+
// A linked record's date and time, in a table of the records a many2many links to.
|
|
446
|
+
if (def.type === 'many2many' && def.fields) {
|
|
447
|
+
for (const [column, linked] of Object.entries(def.fields))
|
|
448
|
+
if (linked.type === 'datetime' && linked.timeZone !== undefined)
|
|
449
|
+
this.checkZone(linked.timeZone, `${path}.fields.${column}.timeZone`);
|
|
450
|
+
}
|
|
395
451
|
if (def.type === 'selection')
|
|
396
452
|
def.optionsFrom?.dependsOn?.forEach((other, i) => this.need(other, `${path}.optionsFrom.dependsOn[${i}]`, fields));
|
|
397
453
|
if (def.type === 'properties')
|
|
@@ -490,9 +546,7 @@ export class ReferenceCheck {
|
|
|
490
546
|
walkList(list, path) {
|
|
491
547
|
if (this.page.data.kind !== 'record')
|
|
492
548
|
this.report('data', 'a list shows records: its data needs kind "record" and a model');
|
|
493
|
-
|
|
494
|
-
list.sort?.forEach((order, i) => this.need(order.field, `${path}.sort[${i}].field`));
|
|
495
|
-
list.searchFields?.forEach((name, i) => this.need(name, `${path}.searchFields[${i}]`));
|
|
549
|
+
this.checkListed(list, this.page.fields, path);
|
|
496
550
|
list.groupBy?.forEach((name, i) => this.need(name, `${path}.groupBy[${i}]`));
|
|
497
551
|
// A list's filters are about its own records: their fields are the page's, and there is no record to take a value from.
|
|
498
552
|
const walk = (item, at) => {
|
|
@@ -508,7 +562,11 @@ export class ReferenceCheck {
|
|
|
508
562
|
list.filters?.forEach((filter, i) => {
|
|
509
563
|
this.claim(filter.id, `${path}.filters[${i}]`);
|
|
510
564
|
named.add(filter.id);
|
|
511
|
-
filter.filter
|
|
565
|
+
filter.filter?.forEach((item, j) => walk(item, `${path}.filters[${i}].filter[${j}]`));
|
|
566
|
+
// A period is of a date, or a date and time.
|
|
567
|
+
const dated = filter.period ? this.need(filter.period.field, `${path}.filters[${i}].period.field`) : undefined;
|
|
568
|
+
if (dated && dated.type !== 'date' && dated.type !== 'datetime')
|
|
569
|
+
this.report(`${path}.filters[${i}].period.field`, `"${filter.period?.field}" is a ${dated.type}; a period is of a date, or a date and time`);
|
|
512
570
|
});
|
|
513
571
|
list.defaultFilters?.forEach((id, i) => {
|
|
514
572
|
if (!named.has(id))
|
|
@@ -523,6 +581,54 @@ export class ReferenceCheck {
|
|
|
523
581
|
if (list.calendar)
|
|
524
582
|
this.walkListCalendar(list, `${path}.calendar`);
|
|
525
583
|
this.checkListCells(list, path);
|
|
584
|
+
this.checkListRows(list, path);
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* A list's cards, gallery, columns people choose and rows' own rules: a
|
|
588
|
+
* card's fields the page's, its picture one that can hold a picture; the
|
|
589
|
+
* optional columns among the columns; a row's tones, bold and buttons
|
|
590
|
+
* reading the list's record (its fields, `id`, `user`); and the list
|
|
591
|
+
* opening on one thing.
|
|
592
|
+
*/
|
|
593
|
+
checkListRows(list, path) {
|
|
594
|
+
if (list.card)
|
|
595
|
+
this.checkListCard(list.card, `${path}.card`, this.page.fields);
|
|
596
|
+
for (const column of Object.keys(list.optionalColumns ?? {})) {
|
|
597
|
+
if (!list.columns.includes(column))
|
|
598
|
+
this.report(`${path}.optionalColumns.${column}`, `"${column}" is no column of this list: a person shows or hides its columns`);
|
|
599
|
+
}
|
|
600
|
+
// A column a person may hide, every one hidden at first: the list would open with no column but its own.
|
|
601
|
+
if (list.optionalColumns && list.columns.every((column) => list.optionalColumns?.[column] === 'hide'))
|
|
602
|
+
this.report(`${path}.optionalColumns`, 'every column starts hidden: show one at least, as the list opens');
|
|
603
|
+
if (list.gallery?.opens === 'gallery') {
|
|
604
|
+
if (list.map?.view && list.map.view !== 'list')
|
|
605
|
+
this.report(`${path}.gallery.opens`, 'the list opens on its map (map.view) and on its gallery: say one');
|
|
606
|
+
if (list.calendar?.opens === 'calendar')
|
|
607
|
+
this.report(`${path}.gallery.opens`, 'the list opens on its calendar and on its gallery: say one');
|
|
608
|
+
}
|
|
609
|
+
list.rowTones?.forEach((tone, i) => this.checkModifiers(tone, `${path}.rowTones[${i}]`, ['when']));
|
|
610
|
+
this.checkModifiers(list, path, ['rowBold']);
|
|
611
|
+
list.rowButtons?.forEach((button, i) => {
|
|
612
|
+
this.claim(button.id, `${path}.rowButtons[${i}]`);
|
|
613
|
+
this.checkPress(button, `${path}.rowButtons[${i}]`);
|
|
614
|
+
// Shown by a condition on each row's record.
|
|
615
|
+
this.checkModifiers(button, `${path}.rowButtons[${i}]`, ['invisible']);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* What a list's card shows — a list page's, or a related list's of its own records' fields: its title, subtitle,
|
|
620
|
+
* status and lines each a field, its picture one that can hold a picture.
|
|
621
|
+
*/
|
|
622
|
+
checkListCard(card, at, fields) {
|
|
623
|
+
for (const key of ['title', 'subtitle', 'status'])
|
|
624
|
+
if (card[key] !== undefined)
|
|
625
|
+
this.need(card[key], `${at}.${key}`, fields);
|
|
626
|
+
if (card.image !== undefined) {
|
|
627
|
+
const def = this.need(card.image, `${at}.image`, fields);
|
|
628
|
+
if (def && !PICTURES.has(def.type))
|
|
629
|
+
this.report(`${at}.image`, `"${card.image}" is a ${def.type}; a card's picture is an image or binary field, or text holding a picture's address`);
|
|
630
|
+
}
|
|
631
|
+
card.fields?.forEach((line, i) => (Array.isArray(line) ? line : [line]).forEach((part, j) => this.need(typeof part === 'string' ? part : part.field, `${at}.fields[${i}]${Array.isArray(line) ? `[${j}]` : ''}${typeof part === 'string' ? '' : '.field'}`, fields)));
|
|
526
632
|
}
|
|
527
633
|
/**
|
|
528
634
|
* A list's calendar: when each record starts (a date, or a date and time)
|
|
@@ -567,22 +673,6 @@ export class ReferenceCheck {
|
|
|
567
673
|
* the rows chosen need a box to choose one.
|
|
568
674
|
*/
|
|
569
675
|
checkListCells(list, path) {
|
|
570
|
-
for (const [column, cell] of Object.entries(list.cells ?? {})) {
|
|
571
|
-
const at = `${path}.cells.${column}`;
|
|
572
|
-
if (!list.columns.includes(column)) {
|
|
573
|
-
this.report(at, `"${column}" is no column of this list: its cells are for its columns`);
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
if (!cell.lead)
|
|
577
|
-
continue;
|
|
578
|
-
const def = this.need(cell.lead.field, `${at}.lead.field`);
|
|
579
|
-
if (def?.type !== 'selection')
|
|
580
|
-
continue;
|
|
581
|
-
const values = new Set(def.options.map((option) => String(option.value)));
|
|
582
|
-
for (const value of Object.keys(cell.lead.looks))
|
|
583
|
-
if (!values.has(value))
|
|
584
|
-
this.report(`${at}.lead.looks.${value}`, `"${value}" is no value of "${cell.lead.field}"`);
|
|
585
|
-
}
|
|
586
676
|
if (list.filterFields && list.search !== 'fields')
|
|
587
677
|
this.report(`${path}.filterFields`, 'the filter fields are for the "fields" search: add "search": "fields"');
|
|
588
678
|
list.filterFields?.forEach((name, i) => {
|
|
@@ -604,6 +694,86 @@ export class ReferenceCheck {
|
|
|
604
694
|
}
|
|
605
695
|
if (list.choose === false && list.actions?.length)
|
|
606
696
|
this.report(`${path}.actions`, 'a list’s buttons act on the rows chosen: with "choose": false there is no way to choose one');
|
|
697
|
+
this.checkBulkEdit(list, path);
|
|
698
|
+
this.checkImport(list, path);
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* What a list shows of its records — a list page's, or a related list's of its own records' fields: its columns, its
|
|
702
|
+
* order and the fields its search looks in, each a field; each column's cells for a column of it, a sum, an average,
|
|
703
|
+
* the lowest and the highest of a number column (any column's count), a lead's icon by a field's values.
|
|
704
|
+
*/
|
|
705
|
+
checkListed(list, fields, path) {
|
|
706
|
+
list.columns.forEach((name, i) => this.need(name, `${path}.columns[${i}]`, fields));
|
|
707
|
+
list.sort?.forEach((order, i) => this.need(order.field, `${path}.sort[${i}].field`, fields));
|
|
708
|
+
list.searchFields?.forEach((name, i) => this.need(name, `${path}.searchFields[${i}]`, fields));
|
|
709
|
+
for (const [column, cell] of Object.entries(list.cells ?? {})) {
|
|
710
|
+
const at = `${path}.cells.${column}`;
|
|
711
|
+
if (!list.columns.includes(column)) {
|
|
712
|
+
this.report(at, `"${column}" is no column of this list: its cells are for its columns`);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const kind = has(fields, column) ? fields[column].type : undefined;
|
|
716
|
+
if (cell.total && cell.total !== 'count' && kind && kind !== 'integer' && kind !== 'float' && kind !== 'monetary') {
|
|
717
|
+
this.report(`${at}.total`, `"${column}" is a ${kind}; its total "${cell.total}" is of numbers — an integer, a float or an amount — and any column's is "count"`);
|
|
718
|
+
}
|
|
719
|
+
if (!cell.lead)
|
|
720
|
+
continue;
|
|
721
|
+
const def = this.need(cell.lead.field, `${at}.lead.field`, fields);
|
|
722
|
+
if (def?.type !== 'selection')
|
|
723
|
+
continue;
|
|
724
|
+
const values = new Set(def.options.map((option) => String(option.value)));
|
|
725
|
+
for (const value of Object.keys(cell.lead.looks))
|
|
726
|
+
if (!values.has(value))
|
|
727
|
+
this.report(`${at}.lead.looks.${value}`, `"${value}" is no value of "${cell.lead.field}"`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/** A list's Edit: each field it names one value can set in many records, and rows to choose to set it on. */
|
|
731
|
+
checkBulkEdit(list, path) {
|
|
732
|
+
if (!list.bulkEdit)
|
|
733
|
+
return;
|
|
734
|
+
if (list.choose === false)
|
|
735
|
+
this.report(`${path}.bulkEdit`, 'Edit changes the rows chosen: with "choose": false there is no way to choose one');
|
|
736
|
+
if (list.bulkEdit === true) {
|
|
737
|
+
if (!list.columns.some((name) => bulkSettable(this.page.fields[name])))
|
|
738
|
+
this.report(`${path}.bulkEdit`, 'Edit offers the columns one value can set, and none of these can: name the fields it offers');
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const named = list.bulkEdit;
|
|
742
|
+
named.forEach((name, i) => {
|
|
743
|
+
const at = `${path}.bulkEdit[${i}]`;
|
|
744
|
+
if (named.indexOf(name) !== i)
|
|
745
|
+
this.report(at, `"${name}" is offered twice`);
|
|
746
|
+
const def = this.need(name, at);
|
|
747
|
+
if (!def)
|
|
748
|
+
return;
|
|
749
|
+
if (!BULK_KINDS.has(def.type))
|
|
750
|
+
this.report(at, `"${name}" is a ${def.type}; one value set in many records is words, a number, money, a yes or no, a date, a choice or a link`);
|
|
751
|
+
else if (def.readonly === true || def.compute)
|
|
752
|
+
this.report(at, `"${name}" is ${def.compute ? 'worked out from other fields' : 'read-only'}; Edit sets a field people can change`);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
/** A list's Import: each field it names one a file's cell can fill, and at least one such field on the page. */
|
|
756
|
+
checkImport(list, path) {
|
|
757
|
+
if (!list.import)
|
|
758
|
+
return;
|
|
759
|
+
if (list.import === true) {
|
|
760
|
+
if (!Object.values(this.page.fields).some((def) => importable(def)))
|
|
761
|
+
this.report(`${path}.import`, 'Import fills the fields a file’s cell can, and this page has none: words, a number, money, yes or no, a date, a choice or a link people can change');
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const named = list.import;
|
|
765
|
+
named.forEach((name, i) => {
|
|
766
|
+
const at = `${path}.import[${i}]`;
|
|
767
|
+
if (named.indexOf(name) !== i)
|
|
768
|
+
this.report(at, `"${name}" is offered twice`);
|
|
769
|
+
const def = this.need(name, at);
|
|
770
|
+
if (!def)
|
|
771
|
+
return;
|
|
772
|
+
if (!IMPORT_KINDS.has(def.type))
|
|
773
|
+
this.report(at, `"${name}" is a ${def.type}; a file's cell fills words, a number, money, a yes or no, a date, a choice, or a link or links by name`);
|
|
774
|
+
else if (def.readonly === true || def.compute)
|
|
775
|
+
this.report(at, `"${name}" is ${def.compute ? 'worked out from other fields' : 'read-only'}; Import fills a field people can change`);
|
|
776
|
+
});
|
|
607
777
|
}
|
|
608
778
|
/** A list's map: where each record is, and the fields its pin and its card show, each of a type that can. */
|
|
609
779
|
walkListMap(map, path) {
|
|
@@ -799,6 +969,11 @@ export class ReferenceCheck {
|
|
|
799
969
|
return this.checkValuesIn(node.text, `${path}.text`);
|
|
800
970
|
case 'map':
|
|
801
971
|
return this.walkMap(node, path);
|
|
972
|
+
case 'related':
|
|
973
|
+
return this.checkRelated(node, path);
|
|
974
|
+
case 'slot':
|
|
975
|
+
this.slots.add(node.name);
|
|
976
|
+
return;
|
|
802
977
|
default:
|
|
803
978
|
return;
|
|
804
979
|
}
|
|
@@ -820,6 +995,48 @@ export class ReferenceCheck {
|
|
|
820
995
|
else
|
|
821
996
|
this.formNames.set(node.name, path);
|
|
822
997
|
}
|
|
998
|
+
/**
|
|
999
|
+
* A related list: on a record's page, its columns, order, search, cells, card and rows' rules of its own records'
|
|
1000
|
+
* fields, its link to this record a link of theirs to this model where its fields name it, and its filter's
|
|
1001
|
+
* `valueFrom` this record's.
|
|
1002
|
+
*/
|
|
1003
|
+
checkRelated(node, path) {
|
|
1004
|
+
// The quick check reads no shapes: a part with nothing to go by is said here.
|
|
1005
|
+
if (typeof node.model !== 'string' || typeof node.field !== 'string' || !Array.isArray(node.columns) || !node.fields || typeof node.fields !== 'object') {
|
|
1006
|
+
return this.report(path, 'a related list needs the model of its records, their link to this record (field), their fields and its columns');
|
|
1007
|
+
}
|
|
1008
|
+
const model = this.page.data.kind === 'record' ? this.page.data.model : null;
|
|
1009
|
+
if (!model || this.line)
|
|
1010
|
+
this.report(path, 'a related list shows the records that point at a record: it belongs on a record’s page');
|
|
1011
|
+
const fields = node.fields;
|
|
1012
|
+
this.checkListed(node, fields, path);
|
|
1013
|
+
// On a phone its rows are cards, as a list's: of its own records' fields.
|
|
1014
|
+
if (node.card)
|
|
1015
|
+
this.checkListCard(node.card, `${path}.card`, fields);
|
|
1016
|
+
// Its rows' own rules, as a list's, each read on the row's record: its own fields, its id, the person, the context.
|
|
1017
|
+
const row = { fields };
|
|
1018
|
+
node.rowTones?.forEach((tone, i) => this.checkModifiers(tone, `${path}.rowTones[${i}]`, ['when'], row));
|
|
1019
|
+
this.checkModifiers(node, path, ['rowBold'], row);
|
|
1020
|
+
node.rowButtons?.forEach((button, i) => {
|
|
1021
|
+
this.claim(button.id, `${path}.rowButtons[${i}]`);
|
|
1022
|
+
this.checkPress(button, `${path}.rowButtons[${i}]`);
|
|
1023
|
+
this.checkModifiers(button, `${path}.rowButtons[${i}]`, ['invisible'], row);
|
|
1024
|
+
});
|
|
1025
|
+
const link = has(fields, node.field) ? fields[node.field] : undefined;
|
|
1026
|
+
if (link && model && (link.type !== 'many2one' || link.relation !== model)) {
|
|
1027
|
+
this.report(`${path}.field`, `"${node.field}" ${link.type === 'many2one' ? `links to ${link.relation}` : `is a ${link.type}`}; the records listed point at this one by a many2one to ${model}`);
|
|
1028
|
+
}
|
|
1029
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
1030
|
+
if (def.type === 'monetary' && def.currencyField !== undefined)
|
|
1031
|
+
this.checkCurrency(def.currencyField, `${path}.fields.${name}.currencyField`, fields, undefined, fields);
|
|
1032
|
+
if (def.type === 'datetime' && def.timeZone !== undefined)
|
|
1033
|
+
this.checkZone(def.timeZone, `${path}.fields.${name}.timeZone`);
|
|
1034
|
+
}
|
|
1035
|
+
// Its filter is about its records, by their fields; what it compares with may be read from this record.
|
|
1036
|
+
node.filter?.forEach((item, i) => this.checkFilterItem(item, `${path}.filter[${i}]`, this.here()));
|
|
1037
|
+
if (node.viewAll && node.viewAll.page === this.page.id)
|
|
1038
|
+
this.report(`${path}.viewAll.page`, 'View all opens a list of the records related: this page is the record they point at');
|
|
1039
|
+
}
|
|
823
1040
|
walkTabs(node, path) {
|
|
824
1041
|
node.children.forEach((tab, i) => {
|
|
825
1042
|
const tabPath = `${path}.children[${i}]`;
|
|
@@ -956,6 +1173,23 @@ export class ReferenceCheck {
|
|
|
956
1173
|
if (parts && parts !== 'json')
|
|
957
1174
|
this.report(`${path}.options.partsField`, `"${options['partsField']}" is a ${parts}; an address's parts are kept in a json field`);
|
|
958
1175
|
}
|
|
1176
|
+
/** Booking slots: on a date and time, its end written in another, asked again as the fields it depends on change. */
|
|
1177
|
+
checkSlots(node, def, path) {
|
|
1178
|
+
const options = node.options ?? {};
|
|
1179
|
+
if (def && def.type !== 'datetime')
|
|
1180
|
+
this.report(`${path}.widget`, `slots is for a datetime field; "${node.field}" is a ${def.type}`);
|
|
1181
|
+
const end = options['endField'];
|
|
1182
|
+
if (typeof end === 'string' && has(this.page.fields, end)) {
|
|
1183
|
+
const type = this.page.fields[end].type;
|
|
1184
|
+
if (end === node.field)
|
|
1185
|
+
this.report(`${path}.options.endField`, 'a booking ends in another field than the one it starts in');
|
|
1186
|
+
else if (type !== 'datetime')
|
|
1187
|
+
this.report(`${path}.options.endField`, `"${end}" is a ${type}; a booking's end is kept in a datetime field`);
|
|
1188
|
+
}
|
|
1189
|
+
const on = options['dependsOn'];
|
|
1190
|
+
if (Array.isArray(on))
|
|
1191
|
+
on.forEach((name, i) => typeof name === 'string' && this.need(name, `${path}.options.dependsOn[${i}]`));
|
|
1192
|
+
}
|
|
959
1193
|
/** A picker's own settings, on the kind of field each is for: a path on a link, Fieldia's own list on a choice, a button adding tags, a colour for each state. */
|
|
960
1194
|
checkPicker(node, def, path) {
|
|
961
1195
|
const options = node.options ?? {};
|
|
@@ -1001,6 +1235,13 @@ export class ReferenceCheck {
|
|
|
1001
1235
|
node.tones?.forEach((tone, i) => this.checkModifiers(tone, `${path}.tones[${i}]`, ['when']));
|
|
1002
1236
|
node.placeholderWhen?.forEach((item, i) => this.checkModifiers(item, `${path}.placeholderWhen[${i}]`, ['when']));
|
|
1003
1237
|
this.checkModifiers(node, path, ['bold']);
|
|
1238
|
+
// What the browser fills its box in with: a box names what it holds, an address in parts only whose it is. (That
|
|
1239
|
+
// it is a purpose the standard lists is the format's check, `validatePage`, as other shapes are.)
|
|
1240
|
+
if (typeof node.autocomplete === 'string') {
|
|
1241
|
+
const problem = autofillGroupProblem(node.autocomplete, node.widget === 'address');
|
|
1242
|
+
if (problem)
|
|
1243
|
+
this.report(`${path}.autocomplete`, problem);
|
|
1244
|
+
}
|
|
1004
1245
|
if (def && node.totals) {
|
|
1005
1246
|
if (def.type !== 'one2many') {
|
|
1006
1247
|
this.report(`${path}.totals`, `totals only apply to one2many fields; "${node.field}" is a ${def.type}`);
|
|
@@ -1023,6 +1264,8 @@ export class ReferenceCheck {
|
|
|
1023
1264
|
}
|
|
1024
1265
|
if (node.widget === 'address-search')
|
|
1025
1266
|
this.checkAddressSearch(node, def, path);
|
|
1267
|
+
if (node.widget === 'slots')
|
|
1268
|
+
this.checkSlots(node, def, path);
|
|
1026
1269
|
if (def)
|
|
1027
1270
|
this.checkPicker(node, def, path);
|
|
1028
1271
|
// The order lines load in: fields of its lines.
|