@fieldia/core 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/package.json +1 -1
- package/schema/page.schema.json +3998 -893
- package/src/index.d.mts +1 -1
- package/src/index.d.ts +1 -1
- package/src/index.js +4 -1
- package/src/index.js.map +1 -1
- package/src/index.mjs +1 -1
- package/src/index.mjs.map +1 -1
- package/src/lib/expression/built-ins.d.mts +4 -0
- package/src/lib/expression/built-ins.d.ts +4 -0
- package/src/lib/expression/built-ins.js +8 -0
- package/src/lib/expression/built-ins.js.map +1 -0
- package/src/lib/expression/built-ins.mjs +5 -0
- package/src/lib/expression/built-ins.mjs.map +1 -0
- package/src/lib/expression/evaluator.js +17 -7
- package/src/lib/expression/evaluator.js.map +1 -1
- package/src/lib/expression/evaluator.mjs +17 -7
- package/src/lib/expression/evaluator.mjs.map +1 -1
- package/src/lib/expression/expression.d.mts +7 -0
- package/src/lib/expression/expression.d.ts +7 -0
- package/src/lib/expression/expression.js +2 -0
- package/src/lib/expression/expression.js.map +1 -1
- package/src/lib/expression/expression.mjs +3 -1
- package/src/lib/expression/expression.mjs.map +1 -1
- package/src/lib/expression/functions.d.mts +2 -0
- package/src/lib/expression/functions.d.ts +2 -0
- package/src/lib/expression/functions.js +38 -5
- package/src/lib/expression/functions.js.map +1 -1
- package/src/lib/expression/functions.mjs +38 -5
- package/src/lib/expression/functions.mjs.map +1 -1
- package/src/lib/expression/modifier.d.mts +7 -0
- package/src/lib/expression/modifier.d.ts +7 -0
- package/src/lib/expression/modifier.js +3 -1
- package/src/lib/expression/modifier.js.map +1 -1
- package/src/lib/expression/modifier.mjs +4 -2
- package/src/lib/expression/modifier.mjs.map +1 -1
- package/src/lib/expression/parser.d.mts +8 -1
- package/src/lib/expression/parser.d.ts +8 -1
- package/src/lib/expression/parser.js +30 -2
- package/src/lib/expression/parser.js.map +1 -1
- package/src/lib/expression/parser.mjs +30 -2
- package/src/lib/expression/parser.mjs.map +1 -1
- package/src/lib/expression/reads.d.mts +7 -0
- package/src/lib/expression/reads.d.ts +7 -0
- package/src/lib/expression/reads.js +23 -0
- package/src/lib/expression/reads.js.map +1 -1
- package/src/lib/expression/reads.mjs +21 -0
- package/src/lib/expression/reads.mjs.map +1 -1
- package/src/lib/format/actions.d.mts +486 -2
- package/src/lib/format/actions.d.ts +486 -2
- package/src/lib/format/actions.js +9 -0
- package/src/lib/format/actions.js.map +1 -1
- package/src/lib/format/actions.mjs +9 -0
- package/src/lib/format/actions.mjs.map +1 -1
- package/src/lib/format/check-page.d.mts +7 -2
- package/src/lib/format/check-page.d.ts +7 -2
- package/src/lib/format/check-page.js +6 -3
- package/src/lib/format/check-page.js.map +1 -1
- package/src/lib/format/check-page.mjs +6 -3
- package/src/lib/format/check-page.mjs.map +1 -1
- package/src/lib/format/field.d.mts +3285 -214
- package/src/lib/format/field.d.ts +3285 -214
- package/src/lib/format/field.js +101 -14
- package/src/lib/format/field.js.map +1 -1
- package/src/lib/format/field.mjs +100 -13
- package/src/lib/format/field.mjs.map +1 -1
- package/src/lib/format/hotkeys.d.mts +6 -0
- package/src/lib/format/hotkeys.d.ts +6 -0
- package/src/lib/format/hotkeys.js +11 -0
- package/src/lib/format/hotkeys.js.map +1 -0
- package/src/lib/format/hotkeys.mjs +7 -0
- package/src/lib/format/hotkeys.mjs.map +1 -0
- package/src/lib/format/layout.d.mts +4185 -352
- package/src/lib/format/layout.d.ts +4185 -352
- package/src/lib/format/layout.js +131 -20
- package/src/lib/format/layout.js.map +1 -1
- package/src/lib/format/layout.mjs +130 -19
- package/src/lib/format/layout.mjs.map +1 -1
- package/src/lib/format/page.d.mts +2938 -196
- package/src/lib/format/page.d.ts +2938 -196
- package/src/lib/format/page.js +7 -1
- package/src/lib/format/page.js.map +1 -1
- package/src/lib/format/page.mjs +6 -1
- package/src/lib/format/page.mjs.map +1 -1
- package/src/lib/format/references.d.mts +35 -2
- package/src/lib/format/references.d.ts +35 -2
- package/src/lib/format/references.js +321 -43
- package/src/lib/format/references.js.map +1 -1
- package/src/lib/format/references.mjs +321 -43
- package/src/lib/format/references.mjs.map +1 -1
- package/src/lib/format/themes.d.mts +9 -0
- package/src/lib/format/themes.d.ts +9 -0
- package/src/lib/format/themes.js +13 -0
- package/src/lib/format/themes.js.map +1 -0
- package/src/lib/format/themes.mjs +10 -0
- package/src/lib/format/themes.mjs.map +1 -0
- package/src/lib/format/translate.js +5 -3
- package/src/lib/format/translate.js.map +1 -1
- package/src/lib/format/translate.mjs +5 -3
- package/src/lib/format/translate.mjs.map +1 -1
- package/src/lib/format/words.d.mts +9 -0
- package/src/lib/format/words.d.ts +9 -0
- package/src/lib/format/words.js +35 -0
- package/src/lib/format/words.js.map +1 -0
- package/src/lib/format/words.mjs +30 -0
- package/src/lib/format/words.mjs.map +1 -0
- package/src/lib/record/cells.d.mts +37 -0
- package/src/lib/record/cells.d.ts +37 -0
- package/src/lib/record/cells.js +59 -0
- package/src/lib/record/cells.js.map +1 -0
- package/src/lib/record/cells.mjs +54 -0
- package/src/lib/record/cells.mjs.map +1 -0
- package/src/lib/record/compute.d.mts +5 -4
- package/src/lib/record/compute.d.ts +5 -4
- package/src/lib/record/compute.js +11 -10
- package/src/lib/record/compute.js.map +1 -1
- package/src/lib/record/compute.mjs +12 -11
- package/src/lib/record/compute.mjs.map +1 -1
- package/src/lib/record/data-source.d.mts +32 -2
- package/src/lib/record/data-source.d.ts +32 -2
- package/src/lib/record/data-source.js.map +1 -1
- package/src/lib/record/data-source.mjs.map +1 -1
- package/src/lib/record/env.d.mts +38 -1
- package/src/lib/record/env.d.ts +38 -1
- package/src/lib/record/env.js +41 -3
- package/src/lib/record/env.js.map +1 -1
- package/src/lib/record/env.mjs +38 -3
- package/src/lib/record/env.mjs.map +1 -1
- package/src/lib/record/filter.d.mts +10 -0
- package/src/lib/record/filter.d.ts +10 -0
- package/src/lib/record/filter.js +49 -0
- package/src/lib/record/filter.js.map +1 -1
- package/src/lib/record/filter.mjs +48 -0
- package/src/lib/record/filter.mjs.map +1 -1
- package/src/lib/record/form.d.mts +143 -8
- package/src/lib/record/form.d.ts +143 -8
- package/src/lib/record/form.js +495 -58
- package/src/lib/record/form.js.map +1 -1
- package/src/lib/record/form.mjs +496 -60
- package/src/lib/record/form.mjs.map +1 -1
- package/src/lib/record/locales/ar.js +3 -0
- package/src/lib/record/locales/ar.js.map +1 -1
- package/src/lib/record/locales/ar.mjs +3 -0
- package/src/lib/record/locales/ar.mjs.map +1 -1
- package/src/lib/record/locales/de.js +3 -0
- package/src/lib/record/locales/de.js.map +1 -1
- package/src/lib/record/locales/de.mjs +3 -0
- package/src/lib/record/locales/de.mjs.map +1 -1
- package/src/lib/record/locales/fr.js +3 -0
- package/src/lib/record/locales/fr.js.map +1 -1
- package/src/lib/record/locales/fr.mjs +3 -0
- package/src/lib/record/locales/fr.mjs.map +1 -1
- package/src/lib/record/memory-data-source.d.mts +26 -2
- package/src/lib/record/memory-data-source.d.ts +26 -2
- package/src/lib/record/memory-data-source.js +98 -7
- package/src/lib/record/memory-data-source.js.map +1 -1
- package/src/lib/record/memory-data-source.mjs +98 -7
- package/src/lib/record/memory-data-source.mjs.map +1 -1
- package/src/lib/record/messages.d.mts +5 -0
- package/src/lib/record/messages.d.ts +5 -0
- package/src/lib/record/messages.js +3 -0
- package/src/lib/record/messages.js.map +1 -1
- package/src/lib/record/messages.mjs +3 -0
- package/src/lib/record/messages.mjs.map +1 -1
- package/src/lib/record/roles.d.mts +10 -0
- package/src/lib/record/roles.d.ts +10 -0
- package/src/lib/record/roles.js +21 -0
- package/src/lib/record/roles.js.map +1 -0
- package/src/lib/record/roles.mjs +18 -0
- package/src/lib/record/roles.mjs.map +1 -0
- package/src/lib/record/rules.js +30 -0
- package/src/lib/record/rules.js.map +1 -1
- package/src/lib/record/rules.mjs +31 -1
- package/src/lib/record/rules.mjs.map +1 -1
- package/src/lib/record/run.d.mts +79 -0
- package/src/lib/record/run.d.ts +79 -0
- package/src/lib/record/run.js +105 -0
- package/src/lib/record/run.js.map +1 -1
- package/src/lib/record/run.mjs +105 -0
- package/src/lib/record/run.mjs.map +1 -1
- package/src/lib/record/set-when.d.mts +5 -2
- package/src/lib/record/set-when.d.ts +5 -2
- package/src/lib/record/set-when.js +28 -12
- package/src/lib/record/set-when.js.map +1 -1
- package/src/lib/record/set-when.mjs +30 -14
- package/src/lib/record/set-when.mjs.map +1 -1
- package/src/lib/record/value-map.d.mts +41 -0
- package/src/lib/record/value-map.d.ts +41 -0
- package/src/lib/record/value-map.js +80 -0
- package/src/lib/record/value-map.js.map +1 -0
- package/src/lib/record/value-map.mjs +73 -0
- package/src/lib/record/value-map.mjs.map +1 -0
- package/src/lib/record/values.d.mts +10 -0
- package/src/lib/record/values.d.ts +10 -0
- package/src/lib/record/values.js.map +1 -1
- package/src/lib/record/values.mjs.map +1 -1
- package/src/runtime.d.mts +11 -6
- package/src/runtime.d.ts +11 -6
- package/src/runtime.js +13 -1
- package/src/runtime.js.map +1 -1
- package/src/runtime.mjs +4 -0
- package/src/runtime.mjs.map +1 -1
package/src/lib/record/form.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createForm = createForm;
|
|
4
|
+
exports.notANumber = notANumber;
|
|
4
5
|
const modifier_1 = require("../expression/modifier");
|
|
5
6
|
const functions_1 = require("../expression/functions");
|
|
6
7
|
const check_1 = require("./check");
|
|
@@ -9,6 +10,10 @@ const compute_1 = require("./compute");
|
|
|
9
10
|
const set_when_1 = require("./set-when");
|
|
10
11
|
const rules_1 = require("./rules");
|
|
11
12
|
const env_1 = require("./env");
|
|
13
|
+
const value_map_1 = require("./value-map");
|
|
14
|
+
const filter_1 = require("./filter");
|
|
15
|
+
const roles_1 = require("./roles");
|
|
16
|
+
const cells_1 = require("./cells");
|
|
12
17
|
const messages_1 = require("./messages");
|
|
13
18
|
const data_source_1 = require("./data-source");
|
|
14
19
|
const scheduler_1 = require("./scheduler");
|
|
@@ -38,13 +43,68 @@ function innerForm(options, within) {
|
|
|
38
43
|
if (node.kind === 'field' && rules)
|
|
39
44
|
nodeRules.set(node.id, (0, rules_1.compileRules)(rules));
|
|
40
45
|
}
|
|
46
|
+
/** Tables of lines with rules of their own, by their field node: each read once. */
|
|
47
|
+
const tables = new Map();
|
|
48
|
+
for (const node of index.values()) {
|
|
49
|
+
const source = node.source;
|
|
50
|
+
if (node.kind !== 'field' || page.fields[source.field]?.type !== 'one2many')
|
|
51
|
+
continue;
|
|
52
|
+
if (source.cells || source.rowTones || source.rowBold !== undefined || source.rowButtons || source.lineDelete !== undefined)
|
|
53
|
+
tables.set(node.id, { field: source.field, compiled: (0, cells_1.compileTable)(source) });
|
|
54
|
+
}
|
|
55
|
+
/** Field nodes whose value takes a tone or bold, by id: each read once. */
|
|
56
|
+
const fieldTones = new Map();
|
|
57
|
+
for (const node of index.values()) {
|
|
58
|
+
const source = node.source;
|
|
59
|
+
if (node.kind === 'field' && (source.tones || source.bold !== undefined))
|
|
60
|
+
fieldTones.set(node.id, (0, cells_1.compileFieldTone)(source));
|
|
61
|
+
}
|
|
62
|
+
/** The tables whose lines load in an order of their own fields, by the field: the first node to give one. */
|
|
63
|
+
const lineOrders = new Map();
|
|
64
|
+
for (const node of index.values()) {
|
|
65
|
+
const source = node.source;
|
|
66
|
+
if (node.kind === 'field' && source.order?.length && page.fields[source.field]?.type === 'one2many' && !lineOrders.has(source.field))
|
|
67
|
+
lineOrders.set(source.field, source.order);
|
|
68
|
+
}
|
|
69
|
+
/** Lines as they load, in their tables' orders — only `loaded` fields, when given: the same values when nothing moved. */
|
|
70
|
+
function ordered(values, loaded) {
|
|
71
|
+
let out = values;
|
|
72
|
+
for (const [field, by] of lineOrders) {
|
|
73
|
+
if (loaded && !loaded.includes(field))
|
|
74
|
+
continue;
|
|
75
|
+
const lines = values[field];
|
|
76
|
+
if (!Array.isArray(lines) || lines.length < 2)
|
|
77
|
+
continue;
|
|
78
|
+
const sorted = [...lines].sort((a, b) => compareLines(a.values, b.values, by));
|
|
79
|
+
if (sorted.some((line, i) => line !== lines[i]))
|
|
80
|
+
out = { ...out, [field]: sorted };
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/** The words of each field's empty box chosen by conditions, by its node: read once. */
|
|
85
|
+
const placeholders = new Map();
|
|
86
|
+
const placeholderOf = (id, own) => {
|
|
87
|
+
if (own.placeholderWhen?.length)
|
|
88
|
+
placeholders.set(id, { fixed: own.placeholder, when: own.placeholderWhen.map((item) => ({ text: item.text, holds: (0, modifier_1.compileModifier)(item.when) })) });
|
|
89
|
+
};
|
|
90
|
+
for (const node of index.values())
|
|
91
|
+
if (node.kind === 'field' && 'type' in node.source)
|
|
92
|
+
placeholderOf(node.id, node.source);
|
|
93
|
+
if (page.layout.type === 'sheet' && page.layout.title)
|
|
94
|
+
placeholderOf('#title', page.layout.title);
|
|
41
95
|
/** Whether any rule only warns: without one, there are never warnings to look for. */
|
|
42
96
|
const warns = [...nodeRules.values()].some((rules) => rules.some((compiled) => compiled.rule.level === 'warning'));
|
|
43
97
|
const steps = page.layout.type === 'wizard' ? page.layout.children.map((step) => step.id) : [];
|
|
44
98
|
const draftKey = () => `fieldia:draft:${page.id}:${state.recordId ?? 'new'}`;
|
|
45
99
|
const today = () => (0, functions_1.localDay)(options.now?.() ?? new Date());
|
|
46
|
-
|
|
47
|
-
|
|
100
|
+
// The record's id and the person, as worked-out values read them: the id is the one asked for until the form has its state.
|
|
101
|
+
let started = false;
|
|
102
|
+
let editing = options.editing !== false;
|
|
103
|
+
/** The record a line's form is of, as its conditions read it: worked out once. */
|
|
104
|
+
const parentContext = options.parent ? (0, env_1.recordContext)(options.parent.values, options.parent.fields, { recordId: options.parent.id ?? null, user: options.user, context: options.context }) : undefined;
|
|
105
|
+
const about = () => ({ recordId: started ? state.recordId : (options.recordId ?? null), user: options.user, editing, context: options.context, parent: parentContext });
|
|
106
|
+
const computed = (0, compute_1.compileComputed)(page, today, about);
|
|
107
|
+
const setWhen = (0, set_when_1.compileSetWhen)(page, today, about);
|
|
48
108
|
/** The saved forms placed on the page, by the part's id: each one's answers sit under its name, as JSON. */
|
|
49
109
|
const parts = new Map();
|
|
50
110
|
for (const node of index.values())
|
|
@@ -58,7 +118,75 @@ function innerForm(options, within) {
|
|
|
58
118
|
fields[part.name] = { type: 'json', label: part.title || part.name };
|
|
59
119
|
/** True while answers are handed to an inner form: what it says back then is no change of its own. */
|
|
60
120
|
let syncing = false;
|
|
61
|
-
|
|
121
|
+
/** A new record's values: each field's default, then those worked out from `defaultFrom`, then what the form is given. */
|
|
122
|
+
/** What the record starts with: each field's default, a new one's worked-out first values, and — as the form opens — what it was given. */
|
|
123
|
+
function startingValues(isNew = options.recordId == null, given = true) {
|
|
124
|
+
const values = (0, values_1.initialValues)(fields, today());
|
|
125
|
+
if (isNew) {
|
|
126
|
+
const about = { recordId: null, user: options.user, context: options.context, parent: parentContext };
|
|
127
|
+
Object.assign(values, (0, value_map_1.firstValues)(page.fields, mapScope(values, page.fields, (0, env_1.recordContext)(values, page.fields, about), (0, env_1.expressionEnv)(values, page.fields, today))));
|
|
128
|
+
}
|
|
129
|
+
return given ? ordered({ ...values, ...(0, values_1.structuredCopy)(options.values ?? {}) }) : values;
|
|
130
|
+
}
|
|
131
|
+
/** The name this form knows a record by, from a link to the same model it holds. */
|
|
132
|
+
function knownLabel(relation, id) {
|
|
133
|
+
const values = (started ? state.values : {});
|
|
134
|
+
for (const [name, def] of Object.entries(page.fields)) {
|
|
135
|
+
if ((def.type !== 'many2one' && def.type !== 'many2many') || def.relation !== relation)
|
|
136
|
+
continue;
|
|
137
|
+
const held = values[name];
|
|
138
|
+
const found = (Array.isArray(held) ? held : held ? [held] : []).find((record) => record?.id === id);
|
|
139
|
+
if (found)
|
|
140
|
+
return found.label;
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
function mapScope(values, scopeFields, context, env, parent) {
|
|
145
|
+
return { values, fields: scopeFields, context, env, ...(parent ? { parent } : {}), ...(options.user ? { user: options.user } : {}), labelOf: knownLabel };
|
|
146
|
+
}
|
|
147
|
+
/** Where a value map on the record is read. */
|
|
148
|
+
function recordScope() {
|
|
149
|
+
const { context, env } = reading();
|
|
150
|
+
return mapScope(state.values, page.fields, context, env);
|
|
151
|
+
}
|
|
152
|
+
/** Where a value map on one line is read: the line, with its record as `parent`. */
|
|
153
|
+
function lineScope(field, values) {
|
|
154
|
+
const def = lineField(field);
|
|
155
|
+
const lineFields = def.fields;
|
|
156
|
+
return mapScope(values, lineFields, (0, env_1.lineContext)(values, lineFields, reading().context), { today }, { values: state.values, fields: page.fields });
|
|
157
|
+
}
|
|
158
|
+
function tableOf(nodeId) {
|
|
159
|
+
const table = tables.get(nodeId);
|
|
160
|
+
if (table)
|
|
161
|
+
return table;
|
|
162
|
+
const node = index.get(nodeId);
|
|
163
|
+
if (node?.kind !== 'field' || page.fields[node.field]?.type !== 'one2many')
|
|
164
|
+
throw new Error(`"${nodeId}" is not a table of lines`);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
/** A line of a table as its rules have it now; none, plain. */
|
|
168
|
+
function lineStateOf(nodeId, key) {
|
|
169
|
+
const table = tableOf(nodeId);
|
|
170
|
+
if (!table)
|
|
171
|
+
return { tone: null, bold: false, cells: {}, buttons: {}, deletable: true };
|
|
172
|
+
const line = (state.values[table.field] ?? []).find((l) => l.key === key);
|
|
173
|
+
if (!line)
|
|
174
|
+
throw new Error(`"${table.field}" has no line "${key}"`);
|
|
175
|
+
const def = lineField(table.field);
|
|
176
|
+
return table.compiled.line((0, env_1.lineContext)(line.values, def.fields, reading().context, line.id ?? null), { today }, userRoles);
|
|
177
|
+
}
|
|
178
|
+
function createValuesOf(field, line) {
|
|
179
|
+
if (!line) {
|
|
180
|
+
const def = fieldDef(field);
|
|
181
|
+
return def.type === 'many2one' || def.type === 'many2many' ? (def.createValues ? (0, value_map_1.readLooseMap)(def.createValues, recordScope()) : {}) : {};
|
|
182
|
+
}
|
|
183
|
+
const found = (state.values[line.lines] ?? []).find((l) => l.key === line.key);
|
|
184
|
+
const sub = lineField(line.lines).fields[field];
|
|
185
|
+
if (!found || !sub || (sub.type !== 'many2one' && sub.type !== 'many2many') || !sub.createValues)
|
|
186
|
+
return {};
|
|
187
|
+
return (0, value_map_1.readLooseMap)(sub.createValues, lineScope(line.lines, found.values));
|
|
188
|
+
}
|
|
189
|
+
let baseline = computed.apply(startingValues());
|
|
62
190
|
/** The setWhen conditions holding for the values as they last settled: one starts to hold only against these. */
|
|
63
191
|
let holding = setWhen.holding(baseline);
|
|
64
192
|
let state = {
|
|
@@ -76,7 +204,9 @@ function innerForm(options, within) {
|
|
|
76
204
|
skipped: [],
|
|
77
205
|
saveProblem: null,
|
|
78
206
|
choices: {},
|
|
207
|
+
definitions: {},
|
|
79
208
|
};
|
|
209
|
+
started = true;
|
|
80
210
|
/** Field errors a refused save brought, each kept until its field changes from the value it was refused with. */
|
|
81
211
|
let serverErrors = {};
|
|
82
212
|
let lineKeys = 0;
|
|
@@ -94,6 +224,7 @@ function innerForm(options, within) {
|
|
|
94
224
|
/** The wizard step last told as entered. */
|
|
95
225
|
let told = null;
|
|
96
226
|
let contextCache = null;
|
|
227
|
+
const userRoles = [...(options.user?.roles ?? [])];
|
|
97
228
|
/** The latest load of each field's choices: an answer to an older one is let go. */
|
|
98
229
|
const choiceLoads = {};
|
|
99
230
|
function set(patch) {
|
|
@@ -111,6 +242,9 @@ function innerForm(options, within) {
|
|
|
111
242
|
for (const name in state.choices)
|
|
112
243
|
if (fromList(name).dependsOn?.some((other) => was[other] !== state.values[other]))
|
|
113
244
|
loadChoices(name);
|
|
245
|
+
for (const name in state.definitions)
|
|
246
|
+
if (fromRecord(name).dependsOn?.some((other) => was[other] !== state.values[other]))
|
|
247
|
+
loadDefinitions(name);
|
|
114
248
|
}
|
|
115
249
|
if (showing && state.step !== told)
|
|
116
250
|
tellStep(entering);
|
|
@@ -161,6 +295,24 @@ function innerForm(options, within) {
|
|
|
161
295
|
const source = options.dataSource;
|
|
162
296
|
void track((source?.options ? source.options({ list, values: (0, values_1.structuredCopy)(state.values) }) : Promise.reject(new Error(`No list "${list}"`))).then((loaded) => put({ options: loaded }), (error) => put({ options: shown, error: error.message ?? String(error) })));
|
|
163
297
|
}
|
|
298
|
+
const fromRecord = (name) => page.fields[name].definitionsFrom;
|
|
299
|
+
const definitionLoads = {};
|
|
300
|
+
function loadDefinitions(name) {
|
|
301
|
+
const { list } = fromRecord(name);
|
|
302
|
+
const load = (definitionLoads[name] = (definitionLoads[name] ?? 0) + 1);
|
|
303
|
+
const put = (definitions) => load === definitionLoads[name] && set({ definitions: { ...state.definitions, [name]: definitions } });
|
|
304
|
+
// What was shown stays while the next ones load.
|
|
305
|
+
const shown = state.definitions[name]?.definitions ?? null;
|
|
306
|
+
put({ definitions: shown, loading: true });
|
|
307
|
+
const source = options.dataSource;
|
|
308
|
+
void track((source?.definitions ? source.definitions({ list, values: (0, values_1.structuredCopy)(state.values) }) : Promise.reject(new Error(`No definitions "${list}"`))).then((loaded) => put({ definitions: loaded }), (error) => put({ definitions: shown, error: error.message ?? String(error) })));
|
|
309
|
+
}
|
|
310
|
+
async function addDefinition(name, definition) {
|
|
311
|
+
const { list } = fromRecord(name);
|
|
312
|
+
const definitions = [...(state.definitions[name]?.definitions ?? []), definition];
|
|
313
|
+
set({ definitions: { ...state.definitions, [name]: { definitions } } });
|
|
314
|
+
await track(options.dataSource?.saveDefinitions?.({ list, values: (0, values_1.structuredCopy)(state.values), definitions }) ?? Promise.resolve());
|
|
315
|
+
}
|
|
164
316
|
/** A field as its value is checked: a list's choices are those loaded, and until they are, any choice may be one. */
|
|
165
317
|
function checked(name) {
|
|
166
318
|
const def = page.fields[name];
|
|
@@ -173,11 +325,12 @@ function innerForm(options, within) {
|
|
|
173
325
|
promise.then(done, done);
|
|
174
326
|
return promise;
|
|
175
327
|
}
|
|
176
|
-
/** The values as expressions read them, and what else they read: the lines' rows, today. */
|
|
328
|
+
/** The values as expressions read them, and what else they read: the record's id, the person, the lines' rows, today. */
|
|
177
329
|
function reading() {
|
|
178
|
-
if (contextCache?.values !== state.values) {
|
|
330
|
+
if (contextCache?.values !== state.values || contextCache.recordId !== state.recordId || contextCache.editing !== editing) {
|
|
179
331
|
const values = state.values;
|
|
180
|
-
|
|
332
|
+
const context = (0, env_1.recordContext)(values, page.fields, about());
|
|
333
|
+
contextCache = { values, recordId: state.recordId, editing, context, env: (0, env_1.expressionEnv)(values, page.fields, today) };
|
|
181
334
|
}
|
|
182
335
|
return contextCache;
|
|
183
336
|
}
|
|
@@ -196,9 +349,10 @@ function innerForm(options, within) {
|
|
|
196
349
|
if (!node)
|
|
197
350
|
throw new Error(`The page has no element "${id}"`);
|
|
198
351
|
const { context: ctx, env } = reading();
|
|
199
|
-
|
|
352
|
+
const hidden = (at) => (at ? !(0, roles_1.rolesAllow)(at.roles, userRoles) || at.invisible.evaluate(ctx, env) : false);
|
|
353
|
+
let invisible = hidden(node);
|
|
200
354
|
for (let parent = node.parent; !invisible && parent; parent = index.get(parent)?.parent ?? null) {
|
|
201
|
-
invisible = index.get(parent)
|
|
355
|
+
invisible = hidden(index.get(parent));
|
|
202
356
|
}
|
|
203
357
|
// A section read-only right now makes everything inside it read-only too.
|
|
204
358
|
let readonly = node.readonly.evaluate(ctx, env);
|
|
@@ -293,10 +447,15 @@ function innerForm(options, within) {
|
|
|
293
447
|
for (const line of state.values[name] ?? []) {
|
|
294
448
|
// A section or note answers only for its text, never for what an item needs.
|
|
295
449
|
const kind = (0, values_1.lineKind)(def, line.values);
|
|
450
|
+
// The table's own rules: a cell its line hides asks nothing, one it requires asks for a value.
|
|
451
|
+
const ruled = tables.has(node.id) ? lineStateOf(node.id, line.key).cells : {};
|
|
296
452
|
for (const [sub, subDef] of Object.entries(def.fields)) {
|
|
297
453
|
if (kind && sub !== def.lineKinds?.text)
|
|
298
454
|
continue;
|
|
299
|
-
const
|
|
455
|
+
const cell = ruled[sub];
|
|
456
|
+
if (cell?.invisible || (tables.has(node.id) && tables.get(node.id).compiled.hidden(sub, reading().context, reading().env, userRoles)))
|
|
457
|
+
continue;
|
|
458
|
+
const lineMessage = (0, check_1.checkValue)(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today());
|
|
300
459
|
if (lineMessage)
|
|
301
460
|
errors[`${name}.${line.key}.${sub}`] = lineMessage;
|
|
302
461
|
}
|
|
@@ -358,7 +517,25 @@ function innerForm(options, within) {
|
|
|
358
517
|
/** Write values someone or something changed. `fresh` values are a starting point instead (a restored draft). */
|
|
359
518
|
function writeValues(written, extra = {}, fresh = false) {
|
|
360
519
|
const values = syncParts(fresh ? startFrom(written) : settle(written), fresh ? 'fresh' : 'edit');
|
|
361
|
-
|
|
520
|
+
let errors = Object.keys(state.errors).length ? revalidateShown(values) : state.errors;
|
|
521
|
+
// Words typed where a number goes are said at once, as Flectra marks a box it cannot read; what is missing waits for sending.
|
|
522
|
+
const unread = {};
|
|
523
|
+
for (const name of Object.keys(written)) {
|
|
524
|
+
const def = page.fields[name];
|
|
525
|
+
if (written[name] === state.values[name] || !def)
|
|
526
|
+
continue;
|
|
527
|
+
if (notANumber(def, written[name]))
|
|
528
|
+
unread[name] = (0, check_1.checkValue)(def, written[name], false, messages);
|
|
529
|
+
if (def.type !== 'one2many')
|
|
530
|
+
continue;
|
|
531
|
+
for (const line of written[name] ?? []) {
|
|
532
|
+
for (const [sub, v] of Object.entries(line.values))
|
|
533
|
+
if (notANumber(def.fields[sub], v))
|
|
534
|
+
unread[`${name}.${line.key}.${sub}`] = (0, check_1.checkValue)(def.fields[sub], v, false, messages);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (Object.keys(unread).length)
|
|
538
|
+
errors = { ...errors, ...unread };
|
|
362
539
|
set({ values, dirty: dirtyFields(values), errors, warnings: withValues(values, collectWarnings), ...extra });
|
|
363
540
|
}
|
|
364
541
|
/** Once errors are on screen, keep them current as the person fixes things. */
|
|
@@ -429,6 +606,9 @@ function innerForm(options, within) {
|
|
|
429
606
|
const source = options.dataSource;
|
|
430
607
|
if (!source?.onchange || page.data.kind !== 'record')
|
|
431
608
|
return;
|
|
609
|
+
// Words in a number box are not a value to work anything out from: the server is asked once they read as one.
|
|
610
|
+
if (unreadIn(page.fields[changed], state.values[changed]))
|
|
611
|
+
return;
|
|
432
612
|
const seq = ++onchangeSeq;
|
|
433
613
|
const request = { model: page.data.model, id: state.recordId, changed, values: (0, values_1.structuredCopy)(state.values) };
|
|
434
614
|
void track(source.onchange(request).then((result) => {
|
|
@@ -529,7 +709,8 @@ function innerForm(options, within) {
|
|
|
529
709
|
values: (0, values_1.structuredCopy)(state.values),
|
|
530
710
|
});
|
|
531
711
|
forgetDraft();
|
|
532
|
-
|
|
712
|
+
// Lines the save brings back load again, in their tables' orders, as Flectra's reload after a save.
|
|
713
|
+
const values = syncParts(startFrom(result.values ? ordered({ ...state.values, ...result.values }, Object.keys(result.values)) : state.values), 'start');
|
|
533
714
|
baseline = (0, values_1.structuredCopy)(values);
|
|
534
715
|
set({ status: 'saved', recordId: result.id, values, dirty: [], warnings: withValues(values, collectWarnings) });
|
|
535
716
|
emit('save', { recordId: state.recordId, values: state.values });
|
|
@@ -565,9 +746,18 @@ function innerForm(options, within) {
|
|
|
565
746
|
}
|
|
566
747
|
function addLine(field, values, by) {
|
|
567
748
|
const def = lineField(field);
|
|
568
|
-
const key = `new-${++lineKeys}`;
|
|
569
749
|
const current = state.values[field] ?? [];
|
|
570
|
-
|
|
750
|
+
// A key no line holds: lines given (a line's own lines, back from its form) may hold new-… keys already.
|
|
751
|
+
let key = `new-${++lineKeys}`;
|
|
752
|
+
while (current.some((line) => line.key === key))
|
|
753
|
+
key = `new-${++lineKeys}`;
|
|
754
|
+
// Each line field's default, then what is worked out from its defaultFrom, then the record's lineDefaults, then what the line is given.
|
|
755
|
+
const lineFields = def.fields;
|
|
756
|
+
const start = (0, values_1.initialValues)(lineFields, today());
|
|
757
|
+
Object.assign(start, (0, value_map_1.firstValues)(lineFields, lineScope(field, start)));
|
|
758
|
+
if (def.lineDefaults)
|
|
759
|
+
Object.assign(start, (0, value_map_1.readValueMap)(def.lineDefaults, lineFields, recordScope()));
|
|
760
|
+
const line = { key, values: { ...start, ...(0, values_1.structuredCopy)(values) } };
|
|
571
761
|
// A new line comes last, so it is numbered after the last.
|
|
572
762
|
if (def.sequenceField && line.values[def.sequenceField] == null) {
|
|
573
763
|
const numbers = current.map((l) => l.values[def.sequenceField]).filter((n) => typeof n === 'number');
|
|
@@ -610,30 +800,26 @@ function innerForm(options, within) {
|
|
|
610
800
|
return null;
|
|
611
801
|
return def.type === 'many2one' || def.type === 'many2many' ? def.relation : null;
|
|
612
802
|
}
|
|
613
|
-
async function runCreate(def, name, typed) {
|
|
803
|
+
async function runCreate(def, name, typed, values = {}) {
|
|
614
804
|
const model = creatable(def);
|
|
615
805
|
const source = options.dataSource;
|
|
616
806
|
if (!model || !source?.create)
|
|
617
807
|
throw new Error(`"${name}" cannot make records: its data source has no create, or it is not a link`);
|
|
618
|
-
return track(source.create({ model, name: typed.trim() }));
|
|
808
|
+
return track(source.create({ model, name: typed.trim(), ...(Object.keys(values).length ? { values } : {}) }));
|
|
619
809
|
}
|
|
620
|
-
async function runSearch(def, name, ctx, query, limit, model) {
|
|
621
|
-
if (def.type !== 'many2one' && def.type !== 'many2many' && def.type !== 'reference') {
|
|
622
|
-
throw new Error(`"${name}" is not a many2one, many2many or
|
|
810
|
+
async function runSearch(def, name, ctx, query, limit, model, ids) {
|
|
811
|
+
if (def.type !== 'many2one' && def.type !== 'many2many' && def.type !== 'reference' && def.type !== 'json') {
|
|
812
|
+
throw new Error(`"${name}" is not a many2one, many2many, reference or json`);
|
|
623
813
|
}
|
|
624
|
-
if (def.type === 'reference' && !model)
|
|
814
|
+
if ((def.type === 'reference' || def.type === 'json') && !model)
|
|
625
815
|
throw new Error(`Searching "${name}" needs a model`);
|
|
626
816
|
const source = options.dataSource;
|
|
627
817
|
if (!source?.search)
|
|
628
818
|
return [];
|
|
629
819
|
// Every valueFrom, in groups too, becomes the value it names before the search goes out.
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
? { all: item.all.map(resolve) }
|
|
634
|
-
: { field: item.field, op: item.op, value: (item.valueFrom !== undefined ? ctx[item.valueFrom] ?? null : item.value ?? null) };
|
|
635
|
-
const filter = def.type === 'reference' ? undefined : def.filter?.map(resolve);
|
|
636
|
-
const relation = def.type === 'reference' ? model : def.relation;
|
|
820
|
+
const own = def.type === 'reference' || def.type === 'json' || !def.filter ? undefined : (0, filter_1.resolveFilter)(def.filter, ctx);
|
|
821
|
+
const filter = ids ? [...(own ?? []), { field: 'id', op: 'in', value: ids }] : own;
|
|
822
|
+
const relation = def.type === 'reference' || def.type === 'json' ? model : def.relation;
|
|
637
823
|
return track(source.search({ model: relation, query, ...(filter ? { filter } : {}), limit }));
|
|
638
824
|
}
|
|
639
825
|
function lineField(field) {
|
|
@@ -649,7 +835,8 @@ function innerForm(options, within) {
|
|
|
649
835
|
function optionalStep(id) {
|
|
650
836
|
return index.get(id)?.source?.optional === true;
|
|
651
837
|
}
|
|
652
|
-
|
|
838
|
+
/** Load the record; `again`, as a step's reload: no draft offered, no `open` steps run a second time. */
|
|
839
|
+
async function load(again = false) {
|
|
653
840
|
const source = options.dataSource;
|
|
654
841
|
if (state.recordId == null || page.data.kind !== 'record')
|
|
655
842
|
return;
|
|
@@ -657,18 +844,65 @@ function innerForm(options, within) {
|
|
|
657
844
|
set({ status: 'error', error: 'This page has no data source to load from' });
|
|
658
845
|
return;
|
|
659
846
|
}
|
|
847
|
+
// Only the latest load is taken: a pager pressed twice shows the record it ended on.
|
|
848
|
+
const seq = ++loads;
|
|
660
849
|
set({ status: 'loading', error: null });
|
|
661
850
|
try {
|
|
662
851
|
const loaded = await source.load({ model: page.data.model, id: state.recordId, fields });
|
|
663
|
-
|
|
852
|
+
if (seq !== loads)
|
|
853
|
+
return;
|
|
854
|
+
// Lines load in their tables' orders; where a person moves them after, they stay.
|
|
855
|
+
baseline = syncParts(startFrom(ordered({ ...(0, values_1.initialValues)(fields, today()), ...loaded })), 'start');
|
|
664
856
|
set({ status: 'ready', values: (0, values_1.structuredCopy)(baseline), dirty: [], errors: {}, warnings: withValues(baseline, collectWarnings) });
|
|
857
|
+
if (again)
|
|
858
|
+
return;
|
|
665
859
|
offerDraft();
|
|
666
860
|
void track(opening());
|
|
667
861
|
}
|
|
668
862
|
catch (error) {
|
|
669
|
-
|
|
863
|
+
if (seq === loads)
|
|
864
|
+
set({ status: 'error', error: error.message });
|
|
670
865
|
}
|
|
671
866
|
}
|
|
867
|
+
let loads = 0;
|
|
868
|
+
/** Another record in this form's place, or a new one: what was there let go, the new one loaded and opened. */
|
|
869
|
+
async function openRecord(id) {
|
|
870
|
+
if (draftTimer !== null)
|
|
871
|
+
scheduler.clearTimeout(draftTimer);
|
|
872
|
+
if (autosaveTimer !== null)
|
|
873
|
+
scheduler.clearTimeout(autosaveTimer);
|
|
874
|
+
draftTimer = autosaveTimer = null;
|
|
875
|
+
serverErrors = {};
|
|
876
|
+
told = null;
|
|
877
|
+
showing = false;
|
|
878
|
+
const fresh = { dirty: [], errors: {}, warning: null, warningField: null, error: null, draft: null, step: steps[0] ?? null, skipped: [], saveProblem: null };
|
|
879
|
+
if (id != null) {
|
|
880
|
+
set({ ...fresh, recordId: id, status: 'idle' });
|
|
881
|
+
return load();
|
|
882
|
+
}
|
|
883
|
+
loads++;
|
|
884
|
+
baseline = startFrom(startingValues(true, false));
|
|
885
|
+
const values = syncParts((0, values_1.structuredCopy)(baseline), 'start');
|
|
886
|
+
set({ ...fresh, recordId: null, status: 'ready', values, warnings: withValues(values, collectWarnings) });
|
|
887
|
+
offerDraft();
|
|
888
|
+
await opening();
|
|
889
|
+
}
|
|
890
|
+
/** The record a record step asks the data source about. */
|
|
891
|
+
const record = () => ({ model: page.data.model, id: state.recordId, fields });
|
|
892
|
+
async function post(message) {
|
|
893
|
+
if (options.host?.post) {
|
|
894
|
+
try {
|
|
895
|
+
return (await options.host.post(message)) !== false;
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (!events.get('post')?.size)
|
|
902
|
+
return false;
|
|
903
|
+
emit('post', { recordId: state.recordId, message });
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
672
906
|
const runner = (0, run_1.createRunner)({
|
|
673
907
|
fields,
|
|
674
908
|
values: () => state.values,
|
|
@@ -679,6 +913,12 @@ function innerForm(options, within) {
|
|
|
679
913
|
addLine: (field, values) => void addLine(field, values, 'step'),
|
|
680
914
|
check,
|
|
681
915
|
save: saving,
|
|
916
|
+
async reload() {
|
|
917
|
+
if (state.recordId == null)
|
|
918
|
+
return { reason: 'cannot', message: 'The record is not saved yet: there is nothing to load again' };
|
|
919
|
+
await load(true);
|
|
920
|
+
return state.status === 'error' ? { reason: 'cannot', message: state.error ?? 'The record could not be loaded again' } : null;
|
|
921
|
+
},
|
|
682
922
|
reset: () => form.reset(),
|
|
683
923
|
goTo: goToStep,
|
|
684
924
|
showing: inRun,
|
|
@@ -686,8 +926,50 @@ function innerForm(options, within) {
|
|
|
686
926
|
confirm: options.confirm,
|
|
687
927
|
onAction: options.onAction,
|
|
688
928
|
answered: (request, result) => emit('action', { request, result }),
|
|
929
|
+
changed: () => state.dirty.length > 0,
|
|
930
|
+
...(options.dataSource?.archive && page.data.kind === 'record' ? { archive: (archive) => track((options.dataSource?.archive)({ ...record(), archive })) } : {}),
|
|
931
|
+
...(options.dataSource?.copy && page.data.kind === 'record' ? { copy: async () => (await track((options.dataSource?.copy)(record()))).id } : {}),
|
|
932
|
+
...(options.dataSource?.delete && page.data.kind === 'record' ? { remove: () => track((options.dataSource?.delete)(record())) } : {}),
|
|
933
|
+
async showRecord(id) {
|
|
934
|
+
await openRecord(id);
|
|
935
|
+
return state.status === 'error' ? { reason: 'cannot', message: state.error ?? 'The record could not be loaded' } : null;
|
|
936
|
+
},
|
|
937
|
+
told(event, id) {
|
|
938
|
+
if ('archived' in event)
|
|
939
|
+
emit('archive', { recordId: id, archived: event.archived });
|
|
940
|
+
else if ('copied' in event)
|
|
941
|
+
emit('duplicate', { from: id, recordId: event.copied });
|
|
942
|
+
else {
|
|
943
|
+
emit('delete', { recordId: id });
|
|
944
|
+
// Nobody moved the form on: it starts a new record in the deleted one's place.
|
|
945
|
+
if (state.recordId === id)
|
|
946
|
+
void track(openRecord(null));
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
post,
|
|
950
|
+
wordsOf: (field) => wordsOfValue(fieldDef(field), state.values[field]),
|
|
689
951
|
});
|
|
690
952
|
const moment = (0, run_1.createMoments)((key, list, chain) => runTop(list, { id: key, chain }));
|
|
953
|
+
/** A press of a button: its confirmation first, then its steps and its app action as a final call, in `scope`. */
|
|
954
|
+
function press(source, scope) {
|
|
955
|
+
const list = [...(source.steps ?? [])];
|
|
956
|
+
if (source.action !== undefined)
|
|
957
|
+
list.push({ do: 'call', action: source.action, ...('params' in source && source.params ? { params: source.params } : {}) });
|
|
958
|
+
// A built-in item of the gear menu with nothing of its own: its step, asked about first when it archives or deletes.
|
|
959
|
+
const builtin = 'builtin' in source && !list.length ? source.builtin : undefined;
|
|
960
|
+
if (builtin)
|
|
961
|
+
list.push({ do: builtin });
|
|
962
|
+
const asked = builtin === 'archive' ? messages.archiveConfirm : builtin === 'delete' ? messages.deleteConfirm : undefined;
|
|
963
|
+
const confirm = 'confirm' in source && source.confirm !== undefined ? source.confirm : asked;
|
|
964
|
+
return track((async () => {
|
|
965
|
+
if (confirm && !(await runner.ask(confirm))) {
|
|
966
|
+
const result = { done: false, reason: 'no', message: confirm };
|
|
967
|
+
emit('run', { id: scope.id, steps: list, result });
|
|
968
|
+
return result;
|
|
969
|
+
}
|
|
970
|
+
return runTop(list, scope);
|
|
971
|
+
})());
|
|
972
|
+
}
|
|
691
973
|
const form = {
|
|
692
974
|
page,
|
|
693
975
|
getState: () => state,
|
|
@@ -704,8 +986,19 @@ function innerForm(options, within) {
|
|
|
704
986
|
},
|
|
705
987
|
setValues: (values) => write(values, 'app'),
|
|
706
988
|
node: nodeState,
|
|
989
|
+
openRecord: (id) => track(openRecord(id)),
|
|
990
|
+
post,
|
|
991
|
+
setEditing(next) {
|
|
992
|
+
if (next === editing)
|
|
993
|
+
return;
|
|
994
|
+
editing = next;
|
|
995
|
+
// Nothing of the record changed; what shows of it may have.
|
|
996
|
+
set({});
|
|
997
|
+
},
|
|
707
998
|
fieldReadonly: (name) => lockedField(fieldDef(name)),
|
|
708
999
|
loadChoices,
|
|
1000
|
+
loadDefinitions,
|
|
1001
|
+
addDefinition,
|
|
709
1002
|
problem(name) {
|
|
710
1003
|
fieldDef(name);
|
|
711
1004
|
// Only a field someone can see is asked anything, and required wherever it is shown so.
|
|
@@ -773,13 +1066,21 @@ function innerForm(options, within) {
|
|
|
773
1066
|
},
|
|
774
1067
|
removeLine(field, key) {
|
|
775
1068
|
lineField(field);
|
|
1069
|
+
// A table that keeps this line (its lineDelete does not hold on it) refuses, wherever the call comes from.
|
|
1070
|
+
for (const [nodeId, table] of tables) {
|
|
1071
|
+
if (table.field !== field || (index.get(nodeId)?.source).lineDelete === undefined)
|
|
1072
|
+
continue;
|
|
1073
|
+
if ((state.values[field] ?? []).some((line) => line.key === key) && !lineStateOf(nodeId, key).deletable) {
|
|
1074
|
+
throw new Error(`The line "${key}" of "${field}" cannot be deleted now: its table's lineDelete does not hold on it`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
776
1077
|
const lines = (state.values[field] ?? []).filter((line) => line.key !== key);
|
|
777
1078
|
writeValues({ ...state.values, [field]: lines });
|
|
778
1079
|
afterEdit(field);
|
|
779
1080
|
changed(field, 'person');
|
|
780
1081
|
},
|
|
781
1082
|
async search(field, query, limit = 8, options = {}) {
|
|
782
|
-
return runSearch(fieldDef(field), field, context(), query, limit, options.model);
|
|
1083
|
+
return runSearch(fieldDef(field), field, context(), query, limit, options.model, options.ids);
|
|
783
1084
|
},
|
|
784
1085
|
async previewLine(field, key, values) {
|
|
785
1086
|
lineField(field);
|
|
@@ -787,16 +1088,16 @@ function innerForm(options, within) {
|
|
|
787
1088
|
const line = lines.find((l) => l.key === key);
|
|
788
1089
|
if (!line)
|
|
789
1090
|
throw new Error(`"${field}" has no line "${key}"`);
|
|
790
|
-
const merged = computed.line(field, { ...line.values, ...values });
|
|
1091
|
+
const merged = computed.line(field, { ...line.values, ...values }, state.values);
|
|
791
1092
|
const source = options.dataSource;
|
|
792
1093
|
if (!source?.onchange || page.data.kind !== 'record')
|
|
793
1094
|
return merged;
|
|
794
1095
|
const changed = lines.map((l) => (l.key === key ? { ...l, values: merged } : l));
|
|
795
1096
|
const result = await source.onchange({ model: page.data.model, id: state.recordId, changed: field, values: (0, values_1.structuredCopy)({ ...state.values, [field]: changed }) });
|
|
796
1097
|
const after = (result.values?.[field] ?? []).find((l) => l.key === key);
|
|
797
|
-
return after ? computed.line(field, { ...merged, ...after.values }) : merged;
|
|
1098
|
+
return after ? computed.line(field, { ...merged, ...after.values }, state.values) : merged;
|
|
798
1099
|
},
|
|
799
|
-
async searchLine(field, key, subfield, query, limit = 8) {
|
|
1100
|
+
async searchLine(field, key, subfield, query, limit = 8, options = {}) {
|
|
800
1101
|
const def = lineField(field);
|
|
801
1102
|
const sub = def.fields[subfield];
|
|
802
1103
|
if (!sub)
|
|
@@ -804,12 +1105,13 @@ function innerForm(options, within) {
|
|
|
804
1105
|
const line = (state.values[field] ?? []).find((l) => l.key === key);
|
|
805
1106
|
if (!line)
|
|
806
1107
|
throw new Error(`"${field}" has no line "${key}"`);
|
|
807
|
-
return runSearch(sub, subfield, (0,
|
|
1108
|
+
return runSearch(sub, subfield, (0, env_1.lineContext)(line.values, def.fields, context(), line.id ?? null), query, limit, options.model, options.ids);
|
|
808
1109
|
},
|
|
809
1110
|
canCreate: (field) => creatable(page.fields[field]) !== null,
|
|
810
|
-
quickCreate: (field, typed) => runCreate(page.fields[field], field, typed),
|
|
1111
|
+
quickCreate: (field, typed) => runCreate(page.fields[field], field, typed, page.fields[field] ? createValuesOf(field) : {}),
|
|
811
1112
|
canCreateLine: (field, subfield) => creatable(lineField(field).fields[subfield]) !== null,
|
|
812
|
-
quickCreateLine: (field, subfield, typed) => runCreate(lineField(field).fields[subfield], subfield, typed),
|
|
1113
|
+
quickCreateLine: (field, subfield, typed, key) => runCreate(lineField(field).fields[subfield], subfield, typed, key ? createValuesOf(subfield, { lines: field, key }) : {}),
|
|
1114
|
+
createValues: (field, line) => createValuesOf(field, line),
|
|
813
1115
|
steps: () => steps.filter((id) => !nodeState(id).invisible),
|
|
814
1116
|
next() {
|
|
815
1117
|
if (!state.step)
|
|
@@ -883,21 +1185,85 @@ function innerForm(options, within) {
|
|
|
883
1185
|
throw new Error(`The page has no button "${id}"`);
|
|
884
1186
|
if (nodeState(id).invisible)
|
|
885
1187
|
return { done: false, reason: 'cannot', message: `The button "${id}" is hidden` };
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1188
|
+
// A button for a table's chosen lines runs with them: runLinesAction.
|
|
1189
|
+
if (node.parent && index.get(node.parent)?.source?.selectedButtons?.some((b) => b.id === id)) {
|
|
1190
|
+
return { done: false, reason: 'cannot', message: `The button "${id}" runs with the lines chosen in its table` };
|
|
1191
|
+
}
|
|
1192
|
+
return press(node.source, { id, chain: [], ...(chosen ? { recordIds: [...chosen.recordIds] } : {}) });
|
|
1193
|
+
},
|
|
1194
|
+
async linkedValues(field, ids, columns) {
|
|
1195
|
+
const def = fieldDef(field);
|
|
1196
|
+
if (def.type !== 'many2many')
|
|
1197
|
+
throw new Error(`"${field}" is not a many2many`);
|
|
1198
|
+
const source = options.dataSource;
|
|
1199
|
+
if (!source?.list || !ids.length)
|
|
1200
|
+
return [];
|
|
1201
|
+
const found = await source.list({ model: def.relation, fields: [...columns], filter: [{ field: 'id', op: 'in', value: [...ids] }], sort: [], offset: 0, limit: ids.length });
|
|
1202
|
+
const byId = new Map(found.records.map((record) => [String(record.id), record]));
|
|
1203
|
+
return ids.flatMap((id) => {
|
|
1204
|
+
const record = byId.get(String(id));
|
|
1205
|
+
return record ? [{ id: record.id, values: record.values }] : [];
|
|
1206
|
+
});
|
|
1207
|
+
},
|
|
1208
|
+
fieldTone(nodeId) {
|
|
1209
|
+
const compiled = fieldTones.get(nodeId);
|
|
1210
|
+
if (compiled) {
|
|
1211
|
+
const { context: ctx, env } = reading();
|
|
1212
|
+
return compiled(ctx, env);
|
|
1213
|
+
}
|
|
1214
|
+
if (index.get(nodeId)?.kind !== 'field')
|
|
1215
|
+
throw new Error(`"${nodeId}" is not a field on this page`);
|
|
1216
|
+
return { tone: null, bold: false };
|
|
1217
|
+
},
|
|
1218
|
+
placeholder(nodeId) {
|
|
1219
|
+
const own = placeholders.get(nodeId);
|
|
1220
|
+
if (!own) {
|
|
1221
|
+
const node = index.get(nodeId);
|
|
1222
|
+
if (node?.kind !== 'field')
|
|
1223
|
+
throw new Error(`"${nodeId}" is not a field on this page`);
|
|
1224
|
+
const source = node.source;
|
|
1225
|
+
return nodeId === '#title' && page.layout.type === 'sheet' ? page.layout.title?.placeholder : source.placeholder;
|
|
1226
|
+
}
|
|
1227
|
+
const { context: ctx, env } = reading();
|
|
1228
|
+
return own.when.find((item) => item.holds.evaluate(ctx, env))?.text ?? own.fixed;
|
|
1229
|
+
},
|
|
1230
|
+
lineState: (nodeId, key) => lineStateOf(nodeId, key),
|
|
1231
|
+
columnHidden(nodeId, column) {
|
|
1232
|
+
const table = tableOf(nodeId);
|
|
1233
|
+
const { context, env } = reading();
|
|
1234
|
+
return table ? table.compiled.hidden(column, context, env, userRoles) : false;
|
|
1235
|
+
},
|
|
1236
|
+
async runRowAction(nodeId, buttonId, key) {
|
|
1237
|
+
const table = tableOf(nodeId);
|
|
1238
|
+
const button = table?.compiled.buttons.find((b) => b.id === buttonId);
|
|
1239
|
+
if (!table || !button)
|
|
1240
|
+
throw new Error(`The table "${nodeId}" has no button "${buttonId}"`);
|
|
1241
|
+
if (!lineStateOf(nodeId, key).buttons[buttonId])
|
|
1242
|
+
return { done: false, reason: 'cannot', message: `The button "${buttonId}" is hidden on this line` };
|
|
1243
|
+
const line = (state.values[table.field] ?? []).find((l) => l.key === key);
|
|
1244
|
+
return press(button, { id: buttonId, chain: [], line: { field: table.field, key, values: (0, values_1.structuredCopy)(line.values) } });
|
|
1245
|
+
},
|
|
1246
|
+
async runLinesAction(nodeId, buttonId, keys) {
|
|
1247
|
+
const node = index.get(nodeId);
|
|
1248
|
+
const source = node?.source;
|
|
1249
|
+
const button = source?.selectedButtons?.find((b) => b.id === buttonId);
|
|
1250
|
+
if (!node || !source || !button)
|
|
1251
|
+
throw new Error(`The table "${nodeId}" has no button "${buttonId}" for its chosen lines`);
|
|
1252
|
+
const all = state.values[source.field] ?? [];
|
|
1253
|
+
const chosen = keys.map((key) => all.find((line) => line.key === key) ?? null);
|
|
1254
|
+
const missing = keys.find((_, i) => !chosen[i]);
|
|
1255
|
+
if (missing !== undefined)
|
|
1256
|
+
throw new Error(`"${source.field}" has no line "${missing}"`);
|
|
1257
|
+
if (nodeState(buttonId).invisible)
|
|
1258
|
+
return { done: false, reason: 'cannot', message: `The button "${buttonId}" is hidden` };
|
|
1259
|
+
if (!keys.length)
|
|
1260
|
+
return { done: false, reason: 'cannot', message: `The button "${buttonId}" runs with lines chosen: none is` };
|
|
1261
|
+
const lines = chosen;
|
|
1262
|
+
return press(button, {
|
|
1263
|
+
id: buttonId,
|
|
1264
|
+
chain: [],
|
|
1265
|
+
lines: { field: source.field, keys: [...keys], ids: lines.flatMap((line) => (line.id === undefined ? [] : [line.id])), values: lines.map((line) => (0, values_1.structuredCopy)(line.values)) },
|
|
1266
|
+
});
|
|
901
1267
|
},
|
|
902
1268
|
run: (list, context = {}) => track(runTop(list, { id: context.id ?? 'run', chain: [], ...(context.recordIds ? { recordIds: [...context.recordIds] } : {}) })),
|
|
903
1269
|
shown(tab) {
|
|
@@ -1015,6 +1381,29 @@ function inside(errors, name) {
|
|
|
1015
1381
|
found[key.slice(name.length + 1)] = message;
|
|
1016
1382
|
return found;
|
|
1017
1383
|
}
|
|
1384
|
+
/** One value against another in a table's order: empty and false first, a link by its name, words as people sort them. */
|
|
1385
|
+
function compareValues(a, b) {
|
|
1386
|
+
const empty = (v) => v === null || v === undefined || v === false || v === '';
|
|
1387
|
+
if (empty(a) || empty(b))
|
|
1388
|
+
return empty(a) === empty(b) ? 0 : empty(a) ? -1 : 1;
|
|
1389
|
+
const plain = (v) => (v !== null && typeof v === 'object' && !Array.isArray(v) && 'label' in v ? String(v.label) : v);
|
|
1390
|
+
const x = plain(a);
|
|
1391
|
+
const y = plain(b);
|
|
1392
|
+
if (typeof x === 'number' && typeof y === 'number')
|
|
1393
|
+
return x - y;
|
|
1394
|
+
if (typeof x === 'boolean' && typeof y === 'boolean')
|
|
1395
|
+
return Number(x) - Number(y);
|
|
1396
|
+
return String(x).localeCompare(String(y));
|
|
1397
|
+
}
|
|
1398
|
+
/** Two lines in a table's order: by the first field, then the next. */
|
|
1399
|
+
function compareLines(a, b, by) {
|
|
1400
|
+
for (const item of by) {
|
|
1401
|
+
const found = compareValues(a[item.field], b[item.field]);
|
|
1402
|
+
if (found)
|
|
1403
|
+
return item.desc ? -found : found;
|
|
1404
|
+
}
|
|
1405
|
+
return 0;
|
|
1406
|
+
}
|
|
1018
1407
|
/** A field no one types in: read-only by its definition, or worked out from others. */
|
|
1019
1408
|
function lockedField(def) {
|
|
1020
1409
|
return def.readonly === true || def.compute !== undefined;
|
|
@@ -1053,11 +1442,13 @@ function linkOps(before, now) {
|
|
|
1053
1442
|
function indexLayout(root) {
|
|
1054
1443
|
const index = new Map();
|
|
1055
1444
|
const add = (source, parent, kind, modifiers) => {
|
|
1445
|
+
const roles = source.roles;
|
|
1056
1446
|
index.set(source.id, {
|
|
1057
1447
|
id: source.id,
|
|
1058
1448
|
parent,
|
|
1059
1449
|
kind,
|
|
1060
1450
|
field: modifiers.field,
|
|
1451
|
+
...(roles?.length ? { roles } : {}),
|
|
1061
1452
|
invisible: (0, modifier_1.compileModifier)(modifiers.invisible),
|
|
1062
1453
|
readonly: (0, modifier_1.compileModifier)(modifiers.readonly),
|
|
1063
1454
|
required: (0, modifier_1.compileModifier)(modifiers.required),
|
|
@@ -1068,6 +1459,9 @@ function indexLayout(root) {
|
|
|
1068
1459
|
for (const node of nodes) {
|
|
1069
1460
|
if (node.type === 'field') {
|
|
1070
1461
|
add(node, parent, 'field', { invisible: node.invisible, readonly: node.readonly, required: node.required, field: node.field });
|
|
1462
|
+
// A table's buttons for its chosen lines and in its control row: buttons of the record, inside the table.
|
|
1463
|
+
for (const button of [...(node.selectedButtons ?? []), ...(node.controlButtons ?? [])])
|
|
1464
|
+
add(button, node.id, 'button', { invisible: button.invisible });
|
|
1071
1465
|
}
|
|
1072
1466
|
else if (node.type === 'button') {
|
|
1073
1467
|
add(node, parent, 'button', { invisible: node.invisible });
|
|
@@ -1105,6 +1499,8 @@ function indexLayout(root) {
|
|
|
1105
1499
|
}
|
|
1106
1500
|
else if (root.type === 'sections') {
|
|
1107
1501
|
walk(root.children, root.id);
|
|
1502
|
+
for (const button of root.footer ?? [])
|
|
1503
|
+
add(button, root.id, 'button', { invisible: button.invisible });
|
|
1108
1504
|
}
|
|
1109
1505
|
else if (root.type === 'list') {
|
|
1110
1506
|
// A list holds no fields of its own record: only the buttons for what is selected.
|
|
@@ -1121,23 +1517,64 @@ function indexLayout(root) {
|
|
|
1121
1517
|
shown('#title', root.title?.field);
|
|
1122
1518
|
shown('#subtitle', root.title?.subtitleField);
|
|
1123
1519
|
shown('#avatar', root.title?.avatarField);
|
|
1124
|
-
|
|
1125
|
-
|
|
1520
|
+
// The statusbar shows and hides by its own condition and roles.
|
|
1521
|
+
if (root.statusbar)
|
|
1522
|
+
add({ id: '#statusbar', ...(root.statusbar.roles ? { roles: root.statusbar.roles } : {}) }, root.id, 'field', { field: root.statusbar.field, invisible: root.statusbar.invisible });
|
|
1523
|
+
for (const button of [...(root.buttons ?? []), ...(root.footer ?? [])])
|
|
1126
1524
|
add(button, root.id, 'button', { invisible: button.invisible });
|
|
1127
1525
|
for (const stat of root.statButtons ?? [])
|
|
1128
1526
|
add(stat, root.id, 'stat', { invisible: stat.invisible });
|
|
1129
|
-
|
|
1130
|
-
add(
|
|
1131
|
-
for (const alert of root.alerts ?? [])
|
|
1527
|
+
for (const ribbon of [...(root.ribbon ? [root.ribbon] : []), ...(root.ribbons ?? [])])
|
|
1528
|
+
add(ribbon, root.id, 'other', { invisible: ribbon.invisible });
|
|
1529
|
+
for (const alert of root.alerts ?? []) {
|
|
1132
1530
|
add(alert, root.id, 'other', { invisible: alert.invisible });
|
|
1531
|
+
// An alert's buttons are hidden with it.
|
|
1532
|
+
for (const button of alert.buttons ?? [])
|
|
1533
|
+
add(button, alert.id, 'button', { invisible: button.invisible });
|
|
1534
|
+
}
|
|
1133
1535
|
for (const badge of root.badges ?? [])
|
|
1134
1536
|
add(badge, root.id, 'other', { invisible: badge.invisible });
|
|
1135
|
-
// Fields over and under the title are field nodes like any other.
|
|
1136
|
-
walk([...(root.title?.above ?? []), ...(root.title?.below ?? [])], root.id);
|
|
1537
|
+
// Fields over and under the title, and on its line, are field nodes like any other.
|
|
1538
|
+
walk([...(root.title?.above ?? []), ...(root.title?.before ?? []), ...(root.title?.after ?? []), ...(root.title?.below ?? [])], root.id);
|
|
1137
1539
|
walk(root.children, root.id);
|
|
1138
1540
|
if (root.sidePanel)
|
|
1139
1541
|
add(root.sidePanel, root.id, 'other', { invisible: root.sidePanel.invisible });
|
|
1542
|
+
// The gear menu's items are buttons of the record; Archive shows while it is active, Unarchive while it is not.
|
|
1543
|
+
for (const item of root.toolbar?.menu ?? []) {
|
|
1544
|
+
add(item, root.id, 'button', { invisible: item.invisible });
|
|
1545
|
+
if (item.builtin === 'archive' || item.builtin === 'unarchive') {
|
|
1546
|
+
const own = index.get(item.id);
|
|
1547
|
+
const mine = own.invisible;
|
|
1548
|
+
const archived = item.builtin === 'unarchive';
|
|
1549
|
+
own.invisible = { ...mine, evaluate: (context, env) => (context['active'] === false) !== archived || mine.evaluate(context, env) };
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
if (root.attachmentPreview)
|
|
1553
|
+
add({ id: '#preview', ...root.attachmentPreview }, root.id, 'other', { invisible: root.attachmentPreview.invisible });
|
|
1140
1554
|
}
|
|
1141
1555
|
return index;
|
|
1142
1556
|
}
|
|
1557
|
+
/** A field's value as words in a posted message: a link's name, a choice's label, a yes or no, a number or a date as written. */
|
|
1558
|
+
function wordsOfValue(def, value) {
|
|
1559
|
+
if (def.type === 'boolean')
|
|
1560
|
+
return value === true ? 'Yes' : 'No';
|
|
1561
|
+
if (value === null || value === undefined || value === false)
|
|
1562
|
+
return '';
|
|
1563
|
+
if (def.type === 'selection')
|
|
1564
|
+
return def.options.find((option) => option.value === value)?.label ?? String(value);
|
|
1565
|
+
if (def.type === 'html')
|
|
1566
|
+
return String(value).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1567
|
+
const one = (item) => (item && typeof item === 'object' && 'label' in item ? String(item.label) : item && typeof item === 'object' && 'name' in item ? String(item.name) : String(item));
|
|
1568
|
+
return Array.isArray(value) ? value.map(one).join(', ') : one(value);
|
|
1569
|
+
}
|
|
1570
|
+
/** Words held where a number goes ("4.001234567.5"): a box the person typed in and the browser could not read as one. */
|
|
1571
|
+
function notANumber(def, value) {
|
|
1572
|
+
return !!def && (def.type === 'integer' || def.type === 'float' || def.type === 'monetary') && typeof value === 'string' && value.trim() !== '';
|
|
1573
|
+
}
|
|
1574
|
+
/** A number box holding words, the field's own or a line's cell. */
|
|
1575
|
+
function unreadIn(def, value) {
|
|
1576
|
+
if (def?.type !== 'one2many')
|
|
1577
|
+
return notANumber(def, value);
|
|
1578
|
+
return (value ?? []).some((line) => Object.entries(line.values).some(([sub, v]) => notANumber(def.fields[sub], v)));
|
|
1579
|
+
}
|
|
1143
1580
|
//# sourceMappingURL=form.js.map
|