@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
package/src/lib/record/form.js
CHANGED
|
@@ -5,8 +5,9 @@ exports.workAtRisk = workAtRisk;
|
|
|
5
5
|
exports.createForm = createForm;
|
|
6
6
|
exports.notANumber = notANumber;
|
|
7
7
|
const upgrade_1 = require("../format/upgrade");
|
|
8
|
+
const zoned_1 = require("../format/zoned");
|
|
8
9
|
const modifier_1 = require("../expression/modifier");
|
|
9
|
-
const
|
|
10
|
+
const zones_1 = require("./zones");
|
|
10
11
|
const check_1 = require("./check");
|
|
11
12
|
const inputs_1 = require("./inputs");
|
|
12
13
|
const compute_1 = require("./compute");
|
|
@@ -19,6 +20,7 @@ const roles_1 = require("./roles");
|
|
|
19
20
|
const cells_1 = require("./cells");
|
|
20
21
|
const messages_1 = require("./messages");
|
|
21
22
|
const data_source_1 = require("./data-source");
|
|
23
|
+
const waits_1 = require("./waits");
|
|
22
24
|
const uploads_1 = require("./uploads");
|
|
23
25
|
const scheduler_1 = require("./scheduler");
|
|
24
26
|
const run_1 = require("./run");
|
|
@@ -34,15 +36,23 @@ function unsavedWork(state) {
|
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
38
|
* Whether a way out would lose the person's work without their knowing: a
|
|
37
|
-
* change of theirs the last save did not send
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* change of theirs the last save did not send, or text they left since that
|
|
40
|
+
* does not read ("12a" counts as "12" does: `unsent`), files still on
|
|
41
|
+
* their way, changes Reload set aside, the changes of a save refused for a
|
|
42
|
+
* conflict, waiting for Reload or Keep mine (only a source that keeps
|
|
43
|
+
* versions has one) — and, `how.changes`, any change not saved. What
|
|
44
|
+
* `form.leave()` — the one check every way out goes through — asks about,
|
|
45
|
+
* once the save on its way and the files on theirs settled. Before a
|
|
46
|
+
* record's first save, a change not saved is not at risk: as in 0.20, each
|
|
47
|
+
* way out decides.
|
|
43
48
|
*/
|
|
44
49
|
function workAtRisk(state, how = {}) {
|
|
45
|
-
return (state.unsent ?? []).length > 0 ||
|
|
50
|
+
return ((state.unsent ?? []).length > 0 ||
|
|
51
|
+
state.setAside != null ||
|
|
52
|
+
(state.uploads ?? []).some(uploads_1.uploadGoing) ||
|
|
53
|
+
// The person asked for these to be saved, and someone else's save came first: still theirs, and not saved.
|
|
54
|
+
(state.conflict != null && state.dirty.length > 0) ||
|
|
55
|
+
(how.changes === true && (state.dirty.length > 0 || Object.keys(state.unread ?? {}).length > 0)));
|
|
46
56
|
}
|
|
47
57
|
/**
|
|
48
58
|
* The record state behind a page: values, what changed, what is visible and
|
|
@@ -54,6 +64,8 @@ function createForm(options) {
|
|
|
54
64
|
const page = (0, upgrade_1.upgradePage)(options.page);
|
|
55
65
|
return innerForm(page === options.page ? options : { ...options, page }, []).form;
|
|
56
66
|
}
|
|
67
|
+
/** Whether a run works a person's change out: a page's `change` steps, set off by it, or a run they set off. */
|
|
68
|
+
const worksOut = (chain) => chain.some((key) => key.startsWith('on.change.'));
|
|
57
69
|
/** A line's cell whose line is no longer there. */
|
|
58
70
|
const gone = Symbol('gone');
|
|
59
71
|
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -66,7 +78,8 @@ const asValue = (values) => values;
|
|
|
66
78
|
* its ways out go through that one's check.
|
|
67
79
|
*/
|
|
68
80
|
function innerForm(options, within, place, outer) {
|
|
69
|
-
|
|
81
|
+
// Its time zone written into each date and time field naming none, for whatever reads one from its field alone.
|
|
82
|
+
const page = (0, zoned_1.zonedPage)(options.page);
|
|
70
83
|
const scheduler = options.scheduler ?? scheduler_1.hostScheduler;
|
|
71
84
|
const messages = options.messages ?? messages_1.MESSAGES.en;
|
|
72
85
|
const index = indexLayout(page.layout);
|
|
@@ -130,7 +143,19 @@ function innerForm(options, within, place, outer) {
|
|
|
130
143
|
const warns = [...nodeRules.values()].some((rules) => rules.some((compiled) => compiled.rule.level === 'warning'));
|
|
131
144
|
const steps = page.layout.type === 'wizard' ? page.layout.children.map((step) => step.id) : [];
|
|
132
145
|
const draftKey = () => `fieldia:draft:${page.id}:${state.recordId ?? 'new'}`;
|
|
133
|
-
|
|
146
|
+
/** Today, on the clock of the page's time zone (the browser's own when it names none). */
|
|
147
|
+
const today = () => (0, zones_1.dayIn)(options.now?.() ?? new Date(), page.timeZone);
|
|
148
|
+
const clock = () => (options.now?.() ?? new Date()).getTime();
|
|
149
|
+
/** What a page's bot check saw — its hidden box filled, the captcha's token — and when the page was shown or started again. */
|
|
150
|
+
let bot = { honeypot: false, captcha: null, since: clock() };
|
|
151
|
+
/** What the bot check saw as the answers go; none without one. */
|
|
152
|
+
function botReport() {
|
|
153
|
+
const check = page.botCheck;
|
|
154
|
+
if (!check)
|
|
155
|
+
return undefined;
|
|
156
|
+
const taken = Math.max(0, clock() - bot.since) / 1000;
|
|
157
|
+
return { honeypot: check.honeypot !== false && bot.honeypot, tooFast: taken < (check.minSeconds ?? 3), seconds: Math.round(taken * 10) / 10, ...(check.captcha ? { captcha: bot.captcha } : {}) };
|
|
158
|
+
}
|
|
134
159
|
// 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.
|
|
135
160
|
let started = false;
|
|
136
161
|
let editing = options.editing !== false;
|
|
@@ -140,6 +165,8 @@ function innerForm(options, within, place, outer) {
|
|
|
140
165
|
const editingNow = () => editing && !(started && state.can?.edit === false);
|
|
141
166
|
const about = () => ({ recordId: started ? state.recordId : (options.recordId ?? null), user: options.user, editing: editingNow(), context: options.context, parent: parentContext });
|
|
142
167
|
const computed = (0, compute_1.compileComputed)(page, today, about);
|
|
168
|
+
/** Each value worked out from others — by the page, or by the app as the lines change — and what it waits for. */
|
|
169
|
+
const workedOut = (0, waits_1.workedOutFrom)(page, !!options.dataSource?.onchange && page.data.kind === 'record', !!options.parent);
|
|
143
170
|
const setWhen = (0, set_when_1.compileSetWhen)(page, today, about);
|
|
144
171
|
/** The saved forms placed on the page, by the part's id: each one's answers sit under its name, as JSON. */
|
|
145
172
|
const parts = new Map();
|
|
@@ -292,16 +319,48 @@ function innerForm(options, within, place, outer) {
|
|
|
292
319
|
let generation = 0;
|
|
293
320
|
/** Closed (`dispose`): a run still going stops quietly, as its dialog or page is gone — no other record replaced it. */
|
|
294
321
|
let disposed = false;
|
|
295
|
-
/**
|
|
322
|
+
/** Counted at each reset (the person's Discard): a save on its way then lands as the record it stored. */
|
|
323
|
+
let resets = 0;
|
|
324
|
+
/**
|
|
325
|
+
* THE test every late answer passes (`recordShown`): taken as it is asked for, true while the record it was asked
|
|
326
|
+
* for is still the one shown, as it was. A save's answer, a call's, an onchange's, a name looked up, a line's
|
|
327
|
+
* dialog, a record step's — none is laid on another record, nor over the values the person's Discard let go.
|
|
328
|
+
*/
|
|
329
|
+
/** The data source with a member a record's page asks of it, and the page's model: refused where either is missing. */
|
|
330
|
+
function recordSource(member) {
|
|
331
|
+
const source = options.dataSource;
|
|
332
|
+
if (page.data.kind !== 'record' || !source?.[member])
|
|
333
|
+
throw new Error(`The data source has no ${member}()`);
|
|
334
|
+
return [source, page.data.model];
|
|
335
|
+
}
|
|
336
|
+
function shownNow() {
|
|
337
|
+
const at = generation;
|
|
338
|
+
const reset = resets;
|
|
339
|
+
// A saved form placed in a record is the record's: another shown round it, its answers are laid nowhere either.
|
|
340
|
+
const round = outer?.recordShown();
|
|
341
|
+
return () => !disposed && at === generation && reset === resets && (!round || round());
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The person's claims on the record: the fields whose last write was theirs. A step's, a rule's or the app's
|
|
345
|
+
* write of the whole field takes one over; a write that works the person's change out (the source's onchange, the
|
|
346
|
+
* page's `change` steps it sets off) keeps it.
|
|
347
|
+
*/
|
|
296
348
|
const personal = new Set();
|
|
349
|
+
/**
|
|
350
|
+
* Of a table the person did not write whole: the keys of the lines they changed, added or took away. A line a
|
|
351
|
+
* step adds is none of theirs, and leaves theirs as they are.
|
|
352
|
+
*/
|
|
353
|
+
const personalLines = new Map();
|
|
297
354
|
/** Fields where a file the person picked landed from the store: at risk until a save sends it, a first save or not. */
|
|
298
355
|
const filed = new Set();
|
|
299
356
|
/** The record as the last finished save left it — what it sent and what came back; a page of answers: what it sent. Null before one. */
|
|
300
357
|
let sentAs = null;
|
|
301
|
-
/**
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
358
|
+
/**
|
|
359
|
+
* The records being deleted, by the person's choice, each by its generation: a way out of one of them lets their
|
|
360
|
+
* work in it go, unasked. Another record shown meanwhile is asked about as any.
|
|
361
|
+
*/
|
|
362
|
+
const deleting = new Map();
|
|
363
|
+
const deletingShown = () => (deleting.get(generation) ?? 0) > 0;
|
|
305
364
|
/** Each field's last comparison with what was sent, so a value that did not move is not compared again. */
|
|
306
365
|
const compared = new Map();
|
|
307
366
|
/** Fields showing a file that did not reach the store: shown until it is sent again or taken away, whatever else changes. */
|
|
@@ -439,8 +498,9 @@ function innerForm(options, within, place, outer) {
|
|
|
439
498
|
const was = state.values;
|
|
440
499
|
const shown = state.errors;
|
|
441
500
|
state = { ...state, ...patch };
|
|
442
|
-
// The person's changes the last save did not send, worked out again from the values as they are now
|
|
443
|
-
|
|
501
|
+
// The person's changes the last save did not send, worked out again from the values as they are now, and from the
|
|
502
|
+
// text they left that does not read.
|
|
503
|
+
if (patch.unsent === undefined && (patch.values !== undefined || patch.unread !== undefined)) {
|
|
444
504
|
const unsent = unsentNow();
|
|
445
505
|
if (!sameList(unsent, state.unsent))
|
|
446
506
|
state = { ...state, unsent };
|
|
@@ -469,42 +529,66 @@ function innerForm(options, within, place, outer) {
|
|
|
469
529
|
for (const listener of [...heard])
|
|
470
530
|
listener(payload);
|
|
471
531
|
}
|
|
472
|
-
/**
|
|
473
|
-
|
|
474
|
-
|
|
532
|
+
/**
|
|
533
|
+
* A field written: told to the app, and — a person's change — the page's `change` steps for it run. `claim` says
|
|
534
|
+
* what of the person's it touches: `line`, the key of the one line of a table the person changed; `keep`, a write
|
|
535
|
+
* that leaves the person's claims as they are (a line a step adds, a write working their change out).
|
|
536
|
+
*/
|
|
537
|
+
function changed(field, by, claim = {}) {
|
|
538
|
+
noteOrigin(field, by, claim);
|
|
475
539
|
emit('change', { field, value: state.values[field], values: state.values, by });
|
|
476
540
|
const list = page.on?.change?.[field];
|
|
477
541
|
if (by === 'person' && list)
|
|
478
542
|
void track(moment(`on.change.${field}`, list, [], true));
|
|
479
543
|
}
|
|
480
|
-
/** Another record is shown: nothing of the last one's is the person's work any more. */
|
|
544
|
+
/** Another record is shown: nothing of the last one's is the person's work any more, and its files on their way stop. */
|
|
481
545
|
function newGeneration() {
|
|
482
546
|
generation++;
|
|
483
547
|
personal.clear();
|
|
548
|
+
personalLines.clear();
|
|
484
549
|
filed.clear();
|
|
485
550
|
compared.clear();
|
|
486
551
|
sentAs = null;
|
|
552
|
+
clearFiles();
|
|
487
553
|
}
|
|
488
554
|
/** Whether a field holds a file the store holds that no save sent: its own, or one in a saved form placed here under its name. */
|
|
489
555
|
const filedIn = (name) => filed.has(name) || [...attached.values()].some((part) => part.name === name && part.inner.files.filed());
|
|
490
556
|
/**
|
|
491
557
|
* The person's work no save took, by field, in the page's order: each of
|
|
492
558
|
* their changes that differs from the record as the last save left it —
|
|
493
|
-
*
|
|
559
|
+
* a field they wrote, or a line of a table they changed — and text they
|
|
560
|
+
* left in a box of it that does not read (`unread`), which no save can
|
|
561
|
+
* send; before a first save, only a file the store holds.
|
|
494
562
|
*/
|
|
495
563
|
function unsentNow() {
|
|
496
|
-
|
|
564
|
+
const typed = sentAs ? unreadFields() : null;
|
|
565
|
+
if (!personal.size && !personalLines.size && !typed?.size)
|
|
497
566
|
return [];
|
|
498
567
|
const out = [];
|
|
499
568
|
for (const name of Object.keys(fields)) {
|
|
500
|
-
|
|
569
|
+
// Typed and left unread since a save: the person's, as a change is — a value changed to "12" is, so is "12a".
|
|
570
|
+
if (typed?.has(name)) {
|
|
571
|
+
out.push(name);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const lines = personalLines.get(name);
|
|
575
|
+
if (!personal.has(name) && !lines)
|
|
501
576
|
continue;
|
|
502
577
|
const from = sentAs ?? (filedIn(name) ? baseline : null);
|
|
503
|
-
if (from
|
|
578
|
+
if (!from)
|
|
579
|
+
continue;
|
|
580
|
+
if (lines ? linesDiffer(name, lines, state.values[name], from[name]) : differs(name, state.values[name], from[name]))
|
|
504
581
|
out.push(name);
|
|
505
582
|
}
|
|
506
583
|
return out;
|
|
507
584
|
}
|
|
585
|
+
/** The fields with text left unread in a box of theirs: a field's, a line's cell's, a property's. */
|
|
586
|
+
function unreadFields() {
|
|
587
|
+
const out = new Set();
|
|
588
|
+
for (const path of Object.keys(state.unread ?? {}))
|
|
589
|
+
out.add(path.split('.')[0]);
|
|
590
|
+
return out;
|
|
591
|
+
}
|
|
508
592
|
function differs(name, value, sent) {
|
|
509
593
|
const known = compared.get(name);
|
|
510
594
|
if (known && known.value === value && known.sent === sent)
|
|
@@ -513,18 +597,53 @@ function innerForm(options, within, place, outer) {
|
|
|
513
597
|
compared.set(name, { value, sent, differs: verdict });
|
|
514
598
|
return verdict;
|
|
515
599
|
}
|
|
600
|
+
/** Whether any of these lines of a table differs from the table as sent: changed, added, or taken away. */
|
|
601
|
+
function linesDiffer(name, keys, value, sent) {
|
|
602
|
+
const def = fields[name];
|
|
603
|
+
const find = (lines, key) => (lines ?? []).find((line) => line.key === key);
|
|
604
|
+
for (const key of keys) {
|
|
605
|
+
const now = find(value, key);
|
|
606
|
+
const was = find(sent, key);
|
|
607
|
+
if (!now && !was)
|
|
608
|
+
continue;
|
|
609
|
+
if (!now || !was)
|
|
610
|
+
return true;
|
|
611
|
+
if (now.values === was.values)
|
|
612
|
+
continue;
|
|
613
|
+
for (const sub of new Set([...Object.keys(now.values), ...Object.keys(was.values)])) {
|
|
614
|
+
if (!sameValue(def.fields[sub], now.values[sub], was.values[sub]))
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
516
620
|
/** `unsent` as it is now, told when it changed. */
|
|
517
621
|
function refreshUnsent() {
|
|
518
622
|
const unsent = unsentNow();
|
|
519
623
|
if (!sameList(unsent, state.unsent))
|
|
520
624
|
set({ unsent });
|
|
521
625
|
}
|
|
522
|
-
/**
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
626
|
+
/**
|
|
627
|
+
* A field written, and by whom: the person's change is theirs — the field, or the line of a table they changed —
|
|
628
|
+
* until a step, a rule or the app writes the whole field in its place. A write that leaves their change where it
|
|
629
|
+
* is (`keep`: a line a step adds, a `change` step working out what they typed) leaves it theirs.
|
|
630
|
+
*/
|
|
631
|
+
function noteOrigin(field, by, claim) {
|
|
632
|
+
if (by === 'person') {
|
|
633
|
+
if (claim.line === undefined) {
|
|
634
|
+
personal.add(field);
|
|
635
|
+
personalLines.delete(field);
|
|
636
|
+
}
|
|
637
|
+
else if (!personal.has(field)) {
|
|
638
|
+
const keys = personalLines.get(field) ?? new Set();
|
|
639
|
+
keys.add(claim.line);
|
|
640
|
+
personalLines.set(field, keys);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
else if (!claim.keep) {
|
|
527
644
|
personal.delete(field);
|
|
645
|
+
personalLines.delete(field);
|
|
646
|
+
}
|
|
528
647
|
refreshUnsent();
|
|
529
648
|
}
|
|
530
649
|
/** A wizard step entered: told to the app, and its `show` steps run. */
|
|
@@ -694,10 +813,12 @@ function innerForm(options, within, place, outer) {
|
|
|
694
813
|
const ticks = node.source.widget === 'tick' && page.fields[name].type === 'boolean';
|
|
695
814
|
return ticks && state.values[name] !== true && nodeState(node.id).required ? messages.tick : undefined;
|
|
696
815
|
}
|
|
816
|
+
/** The widget a field is shown with where it keeps its value its own way (a per cent): the first of its nodes'. */
|
|
817
|
+
const shownWidget = (shown) => shown.map((node) => node.source.widget).find((widget) => widget === 'percentage');
|
|
697
818
|
/** A field node's error now: its field's own rules first, then its answer rules. */
|
|
698
819
|
function nodeError(node) {
|
|
699
820
|
const name = node.field;
|
|
700
|
-
return unticked(node) ?? (0, inputs_1.checkInput)(page.fields[name], node.source, state.values[name], messages) ?? (0, check_1.checkValue)(checked(name), state.values[name], nodeState(node.id).required, messages, today()) ?? ruleCheck(node).error;
|
|
821
|
+
return unticked(node) ?? (0, inputs_1.checkInput)(page.fields[name], node.source, state.values[name], messages) ?? (0, check_1.checkValue)(checked(name), state.values[name], nodeState(node.id).required, messages, today(), node.source.widget) ?? ruleCheck(node).error;
|
|
701
822
|
}
|
|
702
823
|
/** Warnings for the visible fields of the page, without publishing them. */
|
|
703
824
|
function collectWarnings() {
|
|
@@ -781,7 +902,7 @@ function innerForm(options, within, place, outer) {
|
|
|
781
902
|
const cell = ruled[sub];
|
|
782
903
|
if (cell?.invisible || (tables.has(node.id) && tables.get(node.id).compiled.hidden(sub, reading().context, reading().env, userRoles)))
|
|
783
904
|
continue;
|
|
784
|
-
const lineMessage = (0, check_1.checkValue)(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today());
|
|
905
|
+
const lineMessage = (0, check_1.checkValue)(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today(), node.source.cells?.[sub]?.widget);
|
|
785
906
|
if (lineMessage)
|
|
786
907
|
errors[`${name}.${line.key}.${sub}`] = lineMessage;
|
|
787
908
|
}
|
|
@@ -846,7 +967,7 @@ function innerForm(options, within, place, outer) {
|
|
|
846
967
|
if ((0, values_1.isEmpty)(fieldDef(field), value))
|
|
847
968
|
return undefined;
|
|
848
969
|
return (shown.map((node) => (0, inputs_1.checkInput)(page.fields[field], node.source, value, messages)).find(Boolean) ??
|
|
849
|
-
besidesRows(field, (0, check_1.checkValue)(checked(field), value, false, messages, today())) ??
|
|
970
|
+
besidesRows(field, (0, check_1.checkValue)(checked(field), value, false, messages, today(), shownWidget(shown))) ??
|
|
850
971
|
shown.map((node) => ruleCheck(node).error).find(Boolean));
|
|
851
972
|
}
|
|
852
973
|
if (parts.length === 2 && page.fields[field]?.type === 'properties')
|
|
@@ -1032,8 +1153,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1032
1153
|
return;
|
|
1033
1154
|
const seq = ++onchangeSeq;
|
|
1034
1155
|
const request = { model: page.data.model, id: state.recordId, changed, values: (0, values_1.structuredCopy)(state.values) };
|
|
1156
|
+
const shown = shownNow();
|
|
1035
1157
|
void track(source.onchange(request).then((result) => {
|
|
1036
|
-
|
|
1158
|
+
// A newer change asked since; or the record it was asked for is gone, or its values let go: none of it is laid.
|
|
1159
|
+
if (seq !== onchangeSeq || !shown())
|
|
1037
1160
|
return;
|
|
1038
1161
|
const values = { ...state.values, ...(result.values ?? {}) };
|
|
1039
1162
|
writeValues(values, { warning: result.warning ?? null, warningField: result.warning ? changed : null });
|
|
@@ -1134,18 +1257,27 @@ function innerForm(options, within, place, outer) {
|
|
|
1134
1257
|
set({ status: 'error', error: problem.message, saveProblem: problem, conflict: null });
|
|
1135
1258
|
return { reason: 'save', message: problem.message };
|
|
1136
1259
|
}
|
|
1260
|
+
// Asked for the record shown now: another shown while it waits below, it saves nothing of that one, nor says anything there.
|
|
1261
|
+
const asked = generation;
|
|
1262
|
+
const elsewhere = () => ({ reason: 'save', message: messages.recordReplaced });
|
|
1137
1263
|
// What is still typed in a box is read first: it is what is saved, or why nothing is. A name still being looked
|
|
1138
1264
|
// up is waited for, so the record it names is what is saved — or, named by none, why nothing is.
|
|
1139
1265
|
const waiting = readTyped();
|
|
1140
1266
|
if (waiting)
|
|
1141
1267
|
await waiting;
|
|
1268
|
+
if (asked !== generation)
|
|
1269
|
+
return elsewhere();
|
|
1142
1270
|
// Files still on their way are waited for first — a saved form's placed here too: the state says how many, so the page can say why it waits.
|
|
1143
1271
|
if (filesGoing()) {
|
|
1144
1272
|
set({ status: 'saving', error: null, saveProblem: null });
|
|
1145
1273
|
await filesSettled();
|
|
1274
|
+
if (asked !== generation)
|
|
1275
|
+
return elsewhere();
|
|
1146
1276
|
set({ status: 'ready' });
|
|
1147
1277
|
}
|
|
1148
1278
|
if (page.on?.beforeSave) {
|
|
1279
|
+
// Its run stops of itself on a record shown by anything but its own steps (a `call`'s `{ record }`, a reload):
|
|
1280
|
+
// those move the save on with it.
|
|
1149
1281
|
const before = await moment('on.beforeSave', page.on.beforeSave, chain, false);
|
|
1150
1282
|
if (!before.done) {
|
|
1151
1283
|
// A check's problems, or an inner save's, are shown already; any other stop is this save's problem.
|
|
@@ -1170,6 +1302,8 @@ function innerForm(options, within, place, outer) {
|
|
|
1170
1302
|
try {
|
|
1171
1303
|
// Bytes still in the record — a signature drawn, a value the app set, in a saved form placed here too — go to the store first, where it keeps files.
|
|
1172
1304
|
await storeBytes(page.data.kind === 'responses' || state.recordId == null ? Object.keys(fields) : state.dirty);
|
|
1305
|
+
if (at !== generation)
|
|
1306
|
+
return elsewhere();
|
|
1173
1307
|
const failed = fileProblems();
|
|
1174
1308
|
if (Object.keys(failed).length) {
|
|
1175
1309
|
set({ errors: failed, ...unstored(failed) });
|
|
@@ -1181,7 +1315,11 @@ function innerForm(options, within, place, outer) {
|
|
|
1181
1315
|
const sent = shownValues();
|
|
1182
1316
|
const before = state.values;
|
|
1183
1317
|
at = generation;
|
|
1184
|
-
|
|
1318
|
+
const seen = botReport();
|
|
1319
|
+
await source.submit({ pageId: page.id, values: sent, ...(seen ? { bot: seen } : {}) });
|
|
1320
|
+
// The captcha's token is spent once sent: the next send waits for a new one.
|
|
1321
|
+
if (seen?.captcha)
|
|
1322
|
+
bot.captcha = null;
|
|
1185
1323
|
// Answered for answers no longer shown — the form was started again meanwhile: the source has them, the form lays nothing.
|
|
1186
1324
|
if (at !== generation)
|
|
1187
1325
|
return null;
|
|
@@ -1189,7 +1327,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1189
1327
|
// What it sent is what an answer changed since is told against: still to send.
|
|
1190
1328
|
sentAs = before;
|
|
1191
1329
|
set({ status: 'saved', unsent: unsentNow() });
|
|
1192
|
-
emit('send', { values: sent });
|
|
1330
|
+
emit('send', { values: sent, ...(seen ? { bot: seen } : {}) });
|
|
1193
1331
|
void track(moment('on.afterSave', page.on?.afterSave, chain, false));
|
|
1194
1332
|
return null;
|
|
1195
1333
|
}
|
|
@@ -1225,7 +1363,8 @@ function innerForm(options, within, place, outer) {
|
|
|
1225
1363
|
scheduleDraft();
|
|
1226
1364
|
scheduleAutosave();
|
|
1227
1365
|
}
|
|
1228
|
-
|
|
1366
|
+
// The record as this save left it: a change made while it was on its way is no value of it.
|
|
1367
|
+
emit('save', { recordId: state.recordId, values: baseline });
|
|
1229
1368
|
void track(moment('on.afterSave', page.on?.afterSave, chain, false));
|
|
1230
1369
|
return null;
|
|
1231
1370
|
}
|
|
@@ -1281,8 +1420,12 @@ function innerForm(options, within, place, outer) {
|
|
|
1281
1420
|
}
|
|
1282
1421
|
/** The form has its values: told to the app, its `open` steps run, then the wizard step it shows is told. */
|
|
1283
1422
|
async function opening() {
|
|
1423
|
+
const at = generation;
|
|
1284
1424
|
emit('open', { recordId: state.recordId, values: state.values });
|
|
1285
1425
|
await moment('on.open', page.on?.open, [], true);
|
|
1426
|
+
// Another record shown while its open steps ran: that one's opening tells its own steps.
|
|
1427
|
+
if (at !== generation)
|
|
1428
|
+
return;
|
|
1286
1429
|
showing = true;
|
|
1287
1430
|
tellStep([]);
|
|
1288
1431
|
}
|
|
@@ -1303,8 +1446,8 @@ function innerForm(options, within, place, outer) {
|
|
|
1303
1446
|
for (const name of names)
|
|
1304
1447
|
changed(name, 'app');
|
|
1305
1448
|
}
|
|
1306
|
-
/** Write fields — a step's or the app's — worked out once, and tell each. */
|
|
1307
|
-
function write(written, by) {
|
|
1449
|
+
/** Write fields — a step's or the app's — worked out once, and tell each; `keep`, a write working the person's change out. */
|
|
1450
|
+
function write(written, by, keep = false) {
|
|
1308
1451
|
const names = Object.keys(written);
|
|
1309
1452
|
names.forEach(fieldDef);
|
|
1310
1453
|
if (!names.length)
|
|
@@ -1313,8 +1456,9 @@ function innerForm(options, within, place, outer) {
|
|
|
1313
1456
|
for (const name of names)
|
|
1314
1457
|
afterEdit(name);
|
|
1315
1458
|
for (const name of names)
|
|
1316
|
-
changed(name, by);
|
|
1459
|
+
changed(name, by, { keep });
|
|
1317
1460
|
}
|
|
1461
|
+
/** A line added: the person's, theirs; a step's, none of theirs, and the lines they changed stay theirs. */
|
|
1318
1462
|
function addLine(field, values, by) {
|
|
1319
1463
|
const def = lineField(field);
|
|
1320
1464
|
const current = state.values[field] ?? [];
|
|
@@ -1337,7 +1481,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1337
1481
|
const lines = [...current, line];
|
|
1338
1482
|
writeValues({ ...state.values, [field]: lines });
|
|
1339
1483
|
afterEdit(field);
|
|
1340
|
-
changed(field, by);
|
|
1484
|
+
changed(field, by, by === 'person' ? { line: key } : { keep: true });
|
|
1341
1485
|
return key;
|
|
1342
1486
|
}
|
|
1343
1487
|
/**
|
|
@@ -1346,15 +1490,44 @@ function innerForm(options, within, place, outer) {
|
|
|
1346
1490
|
*/
|
|
1347
1491
|
async function check(names) {
|
|
1348
1492
|
names?.forEach(fieldDef);
|
|
1493
|
+
const at = generation;
|
|
1349
1494
|
const waiting = readTyped();
|
|
1350
1495
|
if (waiting)
|
|
1351
1496
|
await waiting;
|
|
1497
|
+
// Another record shown while it waited: nothing found is shown on that one.
|
|
1498
|
+
if (at !== generation)
|
|
1499
|
+
return replaced();
|
|
1352
1500
|
const all = collectErrors();
|
|
1353
1501
|
const found = names ? Object.fromEntries(Object.entries(all).filter(([key]) => names.some((name) => key === name || key.startsWith(`${name}.`)))) : all;
|
|
1354
1502
|
set({ errors: found });
|
|
1355
1503
|
const first = Object.values(found)[0];
|
|
1356
1504
|
return first === undefined ? null : { reason: 'check', message: first };
|
|
1357
1505
|
}
|
|
1506
|
+
/**
|
|
1507
|
+
* What is typed read first, for a run the person or the app set off (`RunScope.typed`): every box read as a save
|
|
1508
|
+
* reads it, a name looked up waited for. Text left that does not read stops the run, said under its box as a check
|
|
1509
|
+
* says it — never a value under it handed on or worked from.
|
|
1510
|
+
*/
|
|
1511
|
+
async function typedFirst() {
|
|
1512
|
+
const at = generation;
|
|
1513
|
+
const waiting = readTyped();
|
|
1514
|
+
if (waiting)
|
|
1515
|
+
await waiting;
|
|
1516
|
+
// Another record shown while it waited: the run is not for that one.
|
|
1517
|
+
if (at !== generation)
|
|
1518
|
+
return replaced();
|
|
1519
|
+
const found = unreadProblems();
|
|
1520
|
+
const first = Object.values(found)[0];
|
|
1521
|
+
if (first === undefined)
|
|
1522
|
+
return null;
|
|
1523
|
+
set({ errors: { ...state.errors, ...found } });
|
|
1524
|
+
return { reason: 'check', message: first };
|
|
1525
|
+
}
|
|
1526
|
+
/** The problems of text left unread in boxes shown — this form's and its saved forms' placed here — by path. */
|
|
1527
|
+
function unreadProblems() {
|
|
1528
|
+
const inner = (path) => [...attached.values()].some((part) => path.startsWith(`${part.name}.`) && !!part.inner.form.getState().unread?.[path.slice(part.name.length + 1)]);
|
|
1529
|
+
return Object.fromEntries(Object.entries(collectErrors()).filter(([path]) => !!state.unread[path] || inner(path)));
|
|
1530
|
+
}
|
|
1358
1531
|
/** Go to a wizard's step for a run; undefined when the target is no step of it, but a tab. */
|
|
1359
1532
|
async function goToStep(target, chain) {
|
|
1360
1533
|
if (!steps.includes(target))
|
|
@@ -1362,16 +1535,19 @@ function innerForm(options, within, place, outer) {
|
|
|
1362
1535
|
if (!form.steps().includes(target))
|
|
1363
1536
|
return { reason: 'cannot', message: `The step "${target}" does not apply now` };
|
|
1364
1537
|
// A name still being looked up in a box on the way is waited for, as a save waits for it.
|
|
1538
|
+
const at = generation;
|
|
1365
1539
|
const waiting = readTyped();
|
|
1366
1540
|
if (waiting)
|
|
1367
1541
|
await waiting;
|
|
1542
|
+
if (at !== generation)
|
|
1543
|
+
return replaced();
|
|
1368
1544
|
let went = false;
|
|
1369
1545
|
inRun(chain, () => (went = form.goTo(target)));
|
|
1370
1546
|
return went ? null : { reason: 'check', message: Object.values(state.errors)[0] };
|
|
1371
1547
|
}
|
|
1372
1548
|
/** Run steps from the top — a button's, a moment's, the app's — and tell how it ended: on the record shown as it starts (`generation`). */
|
|
1373
1549
|
async function runTop(list, scope) {
|
|
1374
|
-
scope.generation
|
|
1550
|
+
scope.generation ??= generation;
|
|
1375
1551
|
const result = await runner.runSteps(list, scope);
|
|
1376
1552
|
emit('run', { id: scope.id, steps: list, result });
|
|
1377
1553
|
return result;
|
|
@@ -1389,7 +1565,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1389
1565
|
throw new Error(`"${name}" cannot make records: its data source has no create, or it is not a link`);
|
|
1390
1566
|
return track(source.create({ model, name: typed.trim(), ...(Object.keys(values).length ? { values } : {}) }));
|
|
1391
1567
|
}
|
|
1392
|
-
async function runSearch(def, name, ctx, query, limit, model, ids) {
|
|
1568
|
+
async function runSearch(def, name, ctx, query, limit, { model, ids, exact } = {}) {
|
|
1393
1569
|
// Formatted text names people and records in place (@mention, #4127): searched by the model they are of.
|
|
1394
1570
|
const byModel = def.type === 'reference' || def.type === 'json' || def.type === 'html';
|
|
1395
1571
|
if (def.type !== 'many2one' && def.type !== 'many2many' && !byModel) {
|
|
@@ -1404,7 +1580,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1404
1580
|
const own = byModel || !def.filter ? undefined : (0, filter_1.resolveFilter)(def.filter, ctx);
|
|
1405
1581
|
const filter = ids ? [...(own ?? []), { field: 'id', op: 'in', value: ids }] : own;
|
|
1406
1582
|
const relation = byModel ? model : def.relation;
|
|
1407
|
-
return track(source.search({ model: relation, query, ...(filter ? { filter } : {}), limit }));
|
|
1583
|
+
return track(source.search({ model: relation, query, ...(filter ? { filter } : {}), limit, ...(exact ? { exact: true } : {}) }));
|
|
1408
1584
|
}
|
|
1409
1585
|
function lineField(field) {
|
|
1410
1586
|
const def = fieldDef(field);
|
|
@@ -1505,7 +1681,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1505
1681
|
}
|
|
1506
1682
|
}
|
|
1507
1683
|
/** Whether a way out goes at once: nothing to wait for, no check going, nothing at risk — as in 0.20. */
|
|
1508
|
-
const atOnce = (how, own = false) => !
|
|
1684
|
+
const atOnce = (how, own = false) => !deletingShown() && !checking && !busy(own) && (how.discard === true || !workAtRisk(state, how));
|
|
1509
1685
|
/** The saves on their way, settled: what a load that is not a way out (a conflict's Reload) waits for. */
|
|
1510
1686
|
async function savesSettled() {
|
|
1511
1687
|
while (saveRunning)
|
|
@@ -1531,10 +1707,13 @@ function innerForm(options, within, place, outer) {
|
|
|
1531
1707
|
if (outer)
|
|
1532
1708
|
return outer.letGo(how, chain);
|
|
1533
1709
|
// A delete the person chose: their work goes with the record, unasked.
|
|
1534
|
-
if (
|
|
1710
|
+
if (deletingShown()) {
|
|
1535
1711
|
forget();
|
|
1536
1712
|
return true;
|
|
1537
1713
|
}
|
|
1714
|
+
// The person's own Discard stops the files on their way, never waits for them.
|
|
1715
|
+
if (how.discard)
|
|
1716
|
+
clearFiles();
|
|
1538
1717
|
const own = chain.includes('on.beforeSave');
|
|
1539
1718
|
if (atOnce(how, own)) {
|
|
1540
1719
|
if (how.discard)
|
|
@@ -1577,13 +1756,46 @@ function innerForm(options, within, place, outer) {
|
|
|
1577
1756
|
forget();
|
|
1578
1757
|
return yes;
|
|
1579
1758
|
}
|
|
1580
|
-
/**
|
|
1759
|
+
/**
|
|
1760
|
+
* Let go of the person's work at risk: files on their way stop, changes set aside go, a conflict's changes are
|
|
1761
|
+
* asked about no more, and the changes no save took go back to the record as the last save left it — so what a
|
|
1762
|
+
* dialog or a page in place hands back once it saved is the record as saved, never the change let go.
|
|
1763
|
+
*/
|
|
1581
1764
|
function forget() {
|
|
1765
|
+
const back = unsentLetGo();
|
|
1582
1766
|
clearFiles();
|
|
1583
1767
|
personal.clear();
|
|
1768
|
+
personalLines.clear();
|
|
1584
1769
|
filed.clear();
|
|
1585
|
-
if (
|
|
1586
|
-
|
|
1770
|
+
if (back)
|
|
1771
|
+
writeValues({ ...state.values, ...back });
|
|
1772
|
+
// Text left that does not read goes too: each box shows what its field holds.
|
|
1773
|
+
const unread = Object.keys(state.unread).length ? { unread: {} } : {};
|
|
1774
|
+
if (state.setAside !== null || state.unsent.length || state.conflict !== null || unread.unread)
|
|
1775
|
+
set({ setAside: null, unsent: [], conflict: null, ...unread });
|
|
1776
|
+
}
|
|
1777
|
+
/** The fields holding the person's changes no save took, as the record as last saved holds them: their lines alone, for a table. */
|
|
1778
|
+
function unsentLetGo() {
|
|
1779
|
+
if (!state.unsent.length)
|
|
1780
|
+
return null;
|
|
1781
|
+
const out = {};
|
|
1782
|
+
for (const name of state.unsent) {
|
|
1783
|
+
const from = sentAs ?? baseline;
|
|
1784
|
+
const keys = personalLines.get(name);
|
|
1785
|
+
out[name] = keys && !personal.has(name) ? linesAsSent(state.values[name], from[name], keys) : (0, values_1.structuredCopy)(from[name]);
|
|
1786
|
+
}
|
|
1787
|
+
return out;
|
|
1788
|
+
}
|
|
1789
|
+
/** A table with these lines as sent: changed ones as they were, added ones gone, ones taken away back in their place. */
|
|
1790
|
+
function linesAsSent(value, sent, keys) {
|
|
1791
|
+
const was = sent ?? [];
|
|
1792
|
+
const of = (key) => was.find((line) => line.key === key);
|
|
1793
|
+
const out = (value ?? []).filter((line) => !keys.has(line.key) || of(line.key)).map((line) => (keys.has(line.key) ? (0, values_1.structuredCopy)(of(line.key)) : line));
|
|
1794
|
+
was.forEach((line, at) => {
|
|
1795
|
+
if (keys.has(line.key) && !out.some((other) => other.key === line.key))
|
|
1796
|
+
out.splice(Math.min(at, out.length), 0, (0, values_1.structuredCopy)(line));
|
|
1797
|
+
});
|
|
1798
|
+
return out;
|
|
1587
1799
|
}
|
|
1588
1800
|
/** The check as a step meets it: null to go on — at once when nothing is at risk — else why the run stops. */
|
|
1589
1801
|
function letGoStep(chain) {
|
|
@@ -1591,12 +1803,19 @@ function innerForm(options, within, place, outer) {
|
|
|
1591
1803
|
return go === true ? null : go.then((yes) => (yes ? null : kept()));
|
|
1592
1804
|
}
|
|
1593
1805
|
const kept = () => ({ reason: 'no', message: messages.discardChanges });
|
|
1594
|
-
/**
|
|
1595
|
-
|
|
1806
|
+
/**
|
|
1807
|
+
* A step's own move — the record loaded again, or another shown — once the check let what is shown go: kept, the
|
|
1808
|
+
* person kept their work; replaced, another record was shown before it ended (the run's steps are not for that one).
|
|
1809
|
+
*/
|
|
1810
|
+
async function ownMove(chain, move) {
|
|
1596
1811
|
const go = letGo({}, chain);
|
|
1597
|
-
if (go
|
|
1598
|
-
return
|
|
1599
|
-
|
|
1812
|
+
if (go !== true && !(await go))
|
|
1813
|
+
return kept();
|
|
1814
|
+
// The record it shows is a generation of its own, made as it starts: another made before it ends replaced it.
|
|
1815
|
+
const moving = move();
|
|
1816
|
+
const mine = generation;
|
|
1817
|
+
await moving;
|
|
1818
|
+
return mine === generation ? null : replaced();
|
|
1600
1819
|
}
|
|
1601
1820
|
/**
|
|
1602
1821
|
* The host's close, as every `close` step reaches it: the form takes it
|
|
@@ -1618,6 +1837,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1618
1837
|
const go = letGo({}, chain);
|
|
1619
1838
|
return go === true ? shut() : go.then((yes) => (yes ? shut() : kept()));
|
|
1620
1839
|
}
|
|
1840
|
+
/** Why a run stops whose record was replaced while it went on — quietly, when the form was closed. */
|
|
1841
|
+
function replaced() {
|
|
1842
|
+
return disposed ? { reason: 'closed' } : { reason: 'cannot', message: messages.recordReplaced };
|
|
1843
|
+
}
|
|
1621
1844
|
/** The record a record step asks the data source about. */
|
|
1622
1845
|
const record = () => ({ model: page.data.model, id: state.recordId, fields });
|
|
1623
1846
|
async function post(message) {
|
|
@@ -1640,18 +1863,30 @@ function innerForm(options, within, place, outer) {
|
|
|
1640
1863
|
recordId: () => state.recordId,
|
|
1641
1864
|
reading,
|
|
1642
1865
|
today,
|
|
1643
|
-
|
|
1866
|
+
// A step's write of a field takes the person's change in it over — unless the step works that change out: the
|
|
1867
|
+
// page's `change` steps, set off by it, as the source's onchange does.
|
|
1868
|
+
write: (values, chain) => write(values, 'step', worksOut(chain)),
|
|
1644
1869
|
store: (values) => store(values),
|
|
1870
|
+
// The person's own change: the field's, or a line of it (a claim by line): put back, the whole field is theirs
|
|
1871
|
+
// again — at risk, asked about — never less than it was.
|
|
1872
|
+
mine: (field) => personal.has(field) || personalLines.has(field),
|
|
1873
|
+
putBack(values, mine) {
|
|
1874
|
+
write(values, 'step');
|
|
1875
|
+
for (const name of mine)
|
|
1876
|
+
noteOrigin(name, 'person', {});
|
|
1877
|
+
},
|
|
1645
1878
|
addLine: (field, values) => void addLine(field, values, 'step'),
|
|
1646
1879
|
check,
|
|
1880
|
+
typedFirst,
|
|
1647
1881
|
save: saving,
|
|
1648
1882
|
generation: () => generation,
|
|
1649
|
-
gone:
|
|
1883
|
+
gone: replaced,
|
|
1650
1884
|
async reload(chain) {
|
|
1651
1885
|
if (state.recordId == null)
|
|
1652
1886
|
return { reason: 'cannot', message: 'The record is not saved yet: there is nothing to load again' };
|
|
1653
|
-
|
|
1654
|
-
|
|
1887
|
+
const stopped = await ownMove(chain, () => load(true));
|
|
1888
|
+
if (stopped)
|
|
1889
|
+
return stopped;
|
|
1655
1890
|
return state.status === 'error' ? { reason: 'cannot', message: state.error ?? 'The record could not be loaded again' } : null;
|
|
1656
1891
|
},
|
|
1657
1892
|
reset(chain) {
|
|
@@ -1672,24 +1907,32 @@ function innerForm(options, within, place, outer) {
|
|
|
1672
1907
|
changed: () => state.dirty.length > 0 || state.uploads.some(uploads_1.uploadGoing),
|
|
1673
1908
|
letGo: letGoStep,
|
|
1674
1909
|
async deleting(remove, done) {
|
|
1675
|
-
|
|
1910
|
+
const of = generation;
|
|
1911
|
+
const shown = shownNow();
|
|
1912
|
+
deleting.set(of, (deleting.get(of) ?? 0) + 1);
|
|
1676
1913
|
try {
|
|
1677
1914
|
const out = await remove();
|
|
1678
|
-
// Deleted: what the person had not saved in it goes with it. Refused, it stays theirs.
|
|
1679
|
-
|
|
1915
|
+
// Deleted: what the person had not saved in it goes with it. Refused, it stays theirs. Another record shown
|
|
1916
|
+
// meanwhile keeps what was typed in it.
|
|
1917
|
+
if (done(out) && shown())
|
|
1680
1918
|
forget();
|
|
1681
1919
|
return out;
|
|
1682
1920
|
}
|
|
1683
1921
|
finally {
|
|
1684
|
-
|
|
1922
|
+
const left = (deleting.get(of) ?? 1) - 1;
|
|
1923
|
+
if (left)
|
|
1924
|
+
deleting.set(of, left);
|
|
1925
|
+
else
|
|
1926
|
+
deleting.delete(of);
|
|
1685
1927
|
}
|
|
1686
1928
|
},
|
|
1687
1929
|
...(options.dataSource?.archive && page.data.kind === 'record' ? { archive: (archive) => track((options.dataSource?.archive)({ ...record(), archive })) } : {}),
|
|
1688
1930
|
...(options.dataSource?.copy && page.data.kind === 'record' ? { copy: async () => (await track((options.dataSource?.copy)(record()))).id } : {}),
|
|
1689
1931
|
...(options.dataSource?.delete && page.data.kind === 'record' ? { remove: () => track((options.dataSource?.delete)(record())) } : {}),
|
|
1690
1932
|
async showRecord(id, chain) {
|
|
1691
|
-
|
|
1692
|
-
|
|
1933
|
+
const stopped = await ownMove(chain, () => showNow(id));
|
|
1934
|
+
if (stopped)
|
|
1935
|
+
return stopped;
|
|
1693
1936
|
return state.status === 'error' ? { reason: 'cannot', message: state.error ?? 'The record could not be loaded' } : null;
|
|
1694
1937
|
},
|
|
1695
1938
|
told(event, id) {
|
|
@@ -1708,8 +1951,24 @@ function innerForm(options, within, place, outer) {
|
|
|
1708
1951
|
wordsOf: (field) => wordsOfValue(fieldDef(field), state.values[field]),
|
|
1709
1952
|
barred: (right) => (state.can?.[right] === false ? state.can.reason ?? messages[MAY_NOT[right]] : null),
|
|
1710
1953
|
});
|
|
1711
|
-
|
|
1712
|
-
|
|
1954
|
+
// A moment's run is on the record it was set off on: one set off again while its run goes, and run after it, is let
|
|
1955
|
+
// go when another record is shown by then (its steps were for that one).
|
|
1956
|
+
const moment = (0, run_1.createMoments)((key, list, chain, on) => runTop(list, { id: key, chain, generation: on }), () => generation);
|
|
1957
|
+
/**
|
|
1958
|
+
* Whether steps do something before their first save — set a status, call the app, open a page — so a record's
|
|
1959
|
+
* button made of them saves first. Steps that start with the save (Save and close), or only check before it, need
|
|
1960
|
+
* no save before them: theirs is the first.
|
|
1961
|
+
*/
|
|
1962
|
+
function actsBeforeSaving(list) {
|
|
1963
|
+
const first = list.findIndex((step) => step.do === 'save');
|
|
1964
|
+
return first > 0 && list.slice(0, first).some((step) => step.do !== 'check');
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* A press of a button: its confirmation first, then its steps and its app action as a final call, in `scope`. A
|
|
1968
|
+
* record's button whose steps act and then save saves the record first when it is new or changed, as Flectra's
|
|
1969
|
+
* button does: refused — a field it requires left empty — none of its steps run, so nothing it does happens to a
|
|
1970
|
+
* record that cannot be saved.
|
|
1971
|
+
*/
|
|
1713
1972
|
function press(source, scope) {
|
|
1714
1973
|
const list = [...(source.steps ?? [])];
|
|
1715
1974
|
if (source.action !== undefined)
|
|
@@ -1720,12 +1979,37 @@ function innerForm(options, within, place, outer) {
|
|
|
1720
1979
|
list.push({ do: builtin });
|
|
1721
1980
|
const asked = builtin === 'archive' ? messages.archiveConfirm : builtin === 'delete' ? messages.deleteConfirm : undefined;
|
|
1722
1981
|
const confirm = 'confirm' in source && source.confirm !== undefined ? source.confirm : asked;
|
|
1982
|
+
// On the record it was pressed on: another shown while its question is asked, it does nothing to that one.
|
|
1983
|
+
scope.generation ??= generation;
|
|
1984
|
+
// Pressed by the person: what is typed is read before a step reads the form or hands its values on.
|
|
1985
|
+
scope.typed ??= 'first';
|
|
1723
1986
|
return track((async () => {
|
|
1724
1987
|
if (confirm && !(await runner.ask(confirm))) {
|
|
1725
1988
|
const result = { done: false, reason: 'no', message: confirm };
|
|
1726
1989
|
emit('run', { id: scope.id, steps: list, result });
|
|
1727
1990
|
return result;
|
|
1728
1991
|
}
|
|
1992
|
+
const saveFirst = page.data.kind === 'record' && actsBeforeSaving(list);
|
|
1993
|
+
// What is typed is read first, as the run reads it before its first step that reads the form (run.ts): text
|
|
1994
|
+
// that does not read stops it with `check`, said under its box — before the save that comes first, and any step.
|
|
1995
|
+
if (saveFirst && scope.typed === 'first') {
|
|
1996
|
+
const unread = await typedFirst();
|
|
1997
|
+
if (unread) {
|
|
1998
|
+
const at = Math.max(0, list.findIndex((step) => step.do !== 'check'));
|
|
1999
|
+
const result = { done: false, stoppedAt: at, step: list[at], reason: unread.reason, ...(unread.message !== undefined ? { message: unread.message } : {}) };
|
|
2000
|
+
emit('run', { id: scope.id, steps: list, result });
|
|
2001
|
+
return result;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
// Then the save first, then the steps.
|
|
2005
|
+
if (saveFirst && (state.recordId == null || state.dirty.length > 0 || Object.keys(state.unread ?? {}).length > 0 || state.uploads.some(uploads_1.uploadGoing))) {
|
|
2006
|
+
const first = await saving(scope.chain);
|
|
2007
|
+
if (first) {
|
|
2008
|
+
const result = { done: false, step: { do: 'save' }, reason: first.reason, ...(first.message !== undefined ? { message: first.message } : {}), ...(first.error !== undefined ? { error: first.error } : {}) };
|
|
2009
|
+
emit('run', { id: scope.id, steps: list, result });
|
|
2010
|
+
return result;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
1729
2013
|
return runTop(list, scope);
|
|
1730
2014
|
})());
|
|
1731
2015
|
}
|
|
@@ -1751,6 +2035,14 @@ function innerForm(options, within, place, outer) {
|
|
|
1751
2035
|
openRecord: (id) => track(openRecord(id)),
|
|
1752
2036
|
leave: (how) => Promise.resolve(letGo(how)),
|
|
1753
2037
|
leaveNow: (how = {}) => (outer ? outer.leaveNow(how) : atOnce(how)),
|
|
2038
|
+
recordShown: shownNow,
|
|
2039
|
+
relatedFilter(id) {
|
|
2040
|
+
const node = index.get(id)?.source;
|
|
2041
|
+
if (node?.type !== 'related' || state.recordId == null)
|
|
2042
|
+
return null;
|
|
2043
|
+
const related = node;
|
|
2044
|
+
return [{ field: related.field, op: '=', value: state.recordId }, ...(related.filter ? (0, filter_1.resolveFilter)(related.filter, context()) : [])];
|
|
2045
|
+
},
|
|
1754
2046
|
post,
|
|
1755
2047
|
setEditing(next) {
|
|
1756
2048
|
if (next === editing)
|
|
@@ -1763,6 +2055,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1763
2055
|
loadChoices,
|
|
1764
2056
|
loadDefinitions,
|
|
1765
2057
|
addDefinition,
|
|
2058
|
+
waitsFor(name) {
|
|
2059
|
+
const from = workedOut.get(name);
|
|
2060
|
+
return from ? (0, waits_1.waitingOn)(page, from, state.values, state.unread) : null;
|
|
2061
|
+
},
|
|
1766
2062
|
problem(name) {
|
|
1767
2063
|
fieldDef(name);
|
|
1768
2064
|
// Only a field someone can see is asked anything, and required wherever it is shown so.
|
|
@@ -1779,7 +2075,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1779
2075
|
}
|
|
1780
2076
|
const own = shown.map(unticked).find(Boolean) ??
|
|
1781
2077
|
shown.map((node) => (0, inputs_1.checkInput)(page.fields[name], node.source, state.values[name], messages)).find(Boolean) ??
|
|
1782
|
-
(0, check_1.checkValue)(checked(name), state.values[name], shown.some((node) => nodeState(node.id).required), messages, today());
|
|
2078
|
+
(0, check_1.checkValue)(checked(name), state.values[name], shown.some((node) => nodeState(node.id).required), messages, today(), shownWidget(shown));
|
|
1783
2079
|
return own ?? shown.map((node) => ruleCheck(node).error).find((error) => error !== undefined) ?? null;
|
|
1784
2080
|
},
|
|
1785
2081
|
setUnread(path, unread) {
|
|
@@ -1825,6 +2121,12 @@ function innerForm(options, within, place, outer) {
|
|
|
1825
2121
|
};
|
|
1826
2122
|
},
|
|
1827
2123
|
settle: () => readTyped() ?? Promise.resolve(),
|
|
2124
|
+
botCheck(seen) {
|
|
2125
|
+
if (seen.honeypot !== undefined)
|
|
2126
|
+
bot.honeypot = seen.honeypot;
|
|
2127
|
+
if (seen.captcha !== undefined)
|
|
2128
|
+
bot.captcha = seen.captcha;
|
|
2129
|
+
},
|
|
1828
2130
|
reading: () => readingNow > 0 || [...attached.values()].some((part) => part.inner.form.reading()),
|
|
1829
2131
|
validate() {
|
|
1830
2132
|
readTyped();
|
|
@@ -1834,6 +2136,50 @@ function innerForm(options, within, place, outer) {
|
|
|
1834
2136
|
},
|
|
1835
2137
|
save: () => track(save()),
|
|
1836
2138
|
canUpload: () => typeof options.dataSource?.upload === 'function',
|
|
2139
|
+
today,
|
|
2140
|
+
recordReads: (id, values, fields) => (0, env_1.recordContext)(values, fields ?? page.fields, { recordId: id, user: options.user, editing: false, context: options.context }),
|
|
2141
|
+
canFindSlots: () => typeof options.dataSource?.slots === 'function',
|
|
2142
|
+
findSlots(field, ask) {
|
|
2143
|
+
const source = options.dataSource;
|
|
2144
|
+
if (!source?.slots)
|
|
2145
|
+
return Promise.reject(new Error('The data source has no slots()'));
|
|
2146
|
+
const def = page.fields[field];
|
|
2147
|
+
const { schedule = field, length, between, from, to } = ask;
|
|
2148
|
+
return source.slots({
|
|
2149
|
+
model: page.data.kind === 'record' ? page.data.model : '',
|
|
2150
|
+
field,
|
|
2151
|
+
recordId: state.recordId,
|
|
2152
|
+
schedule,
|
|
2153
|
+
from,
|
|
2154
|
+
to,
|
|
2155
|
+
timeZone: (def?.type === 'datetime' && def.timeZone) || page.timeZone || (0, zones_1.ownZone)(),
|
|
2156
|
+
...(length ? { length } : {}),
|
|
2157
|
+
...(between ? { between } : {}),
|
|
2158
|
+
values: (0, values_1.structuredCopy)(state.values),
|
|
2159
|
+
});
|
|
2160
|
+
},
|
|
2161
|
+
canFindDuplicates: () => !!(page.data.kind === 'record' && page.duplicates?.fields?.length && options.dataSource?.duplicates),
|
|
2162
|
+
async findDuplicates() {
|
|
2163
|
+
const [source, model] = recordSource('duplicates');
|
|
2164
|
+
const fields = (page.duplicates?.fields ?? []).filter((name) => page.fields[name] && !(0, values_1.isEmpty)(page.fields[name], state.values[name]));
|
|
2165
|
+
const recordId = state.recordId;
|
|
2166
|
+
// Nothing typed in any of them: nothing asked.
|
|
2167
|
+
if (!fields.length)
|
|
2168
|
+
return [];
|
|
2169
|
+
const { matches } = await source.duplicates({ model, recordId, values: (0, values_1.structuredCopy)(state.values), fields });
|
|
2170
|
+
// Never the record itself, whatever the source says; the likest first where it scores them.
|
|
2171
|
+
const others = (matches ?? []).filter((match) => match && String(match.id) !== String(recordId));
|
|
2172
|
+
return others.every((match) => typeof match.score === 'number') ? others.sort((a, b) => b.score - a.score) : others;
|
|
2173
|
+
},
|
|
2174
|
+
canMerge: () => !!(page.data.kind === 'record' && options.dataSource?.merge && options.dataSource.load),
|
|
2175
|
+
async readRecord(id) {
|
|
2176
|
+
const [source, model] = recordSource('load');
|
|
2177
|
+
return (0, data_source_1.loadedRecordOf)(await source.load({ model, id, fields: page.fields })).values;
|
|
2178
|
+
},
|
|
2179
|
+
async merge(request) {
|
|
2180
|
+
const [source, model] = recordSource('merge');
|
|
2181
|
+
return source.merge({ model, keepId: request.keepId, mergeIds: [...request.mergeIds], values: (0, values_1.structuredCopy)(request.values), fields: page.fields });
|
|
2182
|
+
},
|
|
1837
2183
|
upload: (field, files, how = {}) => uploads.start(field, files, how),
|
|
1838
2184
|
uploading: (field) => state.uploads.filter((upload) => upload.field === field && !upload.line),
|
|
1839
2185
|
cancelUpload(id) {
|
|
@@ -1859,8 +2205,11 @@ function innerForm(options, within, place, outer) {
|
|
|
1859
2205
|
// The person's own Discard: nothing of theirs is left at risk, and a save on its way lands as the record it stored.
|
|
1860
2206
|
resets++;
|
|
1861
2207
|
personal.clear();
|
|
2208
|
+
personalLines.clear();
|
|
1862
2209
|
filed.clear();
|
|
1863
2210
|
sentAs = null;
|
|
2211
|
+
// Started again: a new response, timed from now.
|
|
2212
|
+
bot = { ...bot, honeypot: false, since: clock() };
|
|
1864
2213
|
const values = syncParts((0, values_1.structuredCopy)(baseline), 'start');
|
|
1865
2214
|
set({ values, dirty: [], errors: {}, warnings: withValues(values, collectWarnings), warning: null, warningField: null, draft: null, step: steps[0] ?? null, skipped: [], saveProblem: null, conflict: null, unsent: [], unread: {} });
|
|
1866
2215
|
},
|
|
@@ -1873,7 +2222,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1873
2222
|
const lines = (state.values[field] ?? []).map((line) => line.key === key ? { ...line, values: { ...line.values, [name]: (0, values_1.structuredCopy)(value) } } : line);
|
|
1874
2223
|
writeValues({ ...state.values, [field]: lines });
|
|
1875
2224
|
afterEdit(field);
|
|
1876
|
-
changed(field, 'person');
|
|
2225
|
+
changed(field, 'person', { line: key });
|
|
1877
2226
|
},
|
|
1878
2227
|
moveLine(field, key, to) {
|
|
1879
2228
|
const def = lineField(field);
|
|
@@ -1918,10 +2267,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1918
2267
|
const lines = (state.values[field] ?? []).filter((line) => line.key !== key);
|
|
1919
2268
|
writeValues({ ...state.values, [field]: lines });
|
|
1920
2269
|
afterEdit(field);
|
|
1921
|
-
changed(field, 'person');
|
|
2270
|
+
changed(field, 'person', { line: key });
|
|
1922
2271
|
},
|
|
1923
2272
|
async search(field, query, limit = 8, options = {}) {
|
|
1924
|
-
return runSearch(fieldDef(field), field, context(), query, limit, options
|
|
2273
|
+
return runSearch(fieldDef(field), field, context(), query, limit, options);
|
|
1925
2274
|
},
|
|
1926
2275
|
async previewLine(field, key, values) {
|
|
1927
2276
|
lineField(field);
|
|
@@ -1946,7 +2295,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1946
2295
|
const line = (state.values[field] ?? []).find((l) => l.key === key);
|
|
1947
2296
|
if (!line)
|
|
1948
2297
|
throw new Error(`"${field}" has no line "${key}"`);
|
|
1949
|
-
return runSearch(sub, subfield, (0, env_1.lineContext)(line.values, def.fields, context(), line.id ?? null), query, limit, options
|
|
2298
|
+
return runSearch(sub, subfield, (0, env_1.lineContext)(line.values, def.fields, context(), line.id ?? null), query, limit, options);
|
|
1950
2299
|
},
|
|
1951
2300
|
canCreate: (field) => creatable(page.fields[field]) !== null,
|
|
1952
2301
|
quickCreate: (field, typed) => runCreate(page.fields[field], field, typed, page.fields[field] ? createValuesOf(field) : {}),
|
|
@@ -2108,7 +2457,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2108
2457
|
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)) },
|
|
2109
2458
|
});
|
|
2110
2459
|
},
|
|
2111
|
-
run: (list, context = {}) => track(runTop(list, { id: context.id ?? 'run', chain: [], ...(context.recordIds ? { recordIds: [...context.recordIds] } : {}) })),
|
|
2460
|
+
run: (list, context = {}) => track(runTop(list, { id: context.id ?? 'run', chain: [], typed: 'first', ...(context.recordIds ? { recordIds: [...context.recordIds] } : {}) })),
|
|
2112
2461
|
shown(tab) {
|
|
2113
2462
|
// A wizard's steps are told by the form itself, as they are entered.
|
|
2114
2463
|
if (steps.includes(tab))
|
|
@@ -2135,20 +2484,36 @@ function innerForm(options, within, place, outer) {
|
|
|
2135
2484
|
},
|
|
2136
2485
|
async reloadTheirs() {
|
|
2137
2486
|
// A save on its way first: its answer is not dropped by the load.
|
|
2487
|
+
const asked = shownNow();
|
|
2138
2488
|
await savesSettled();
|
|
2139
|
-
|
|
2489
|
+
if (!asked())
|
|
2490
|
+
return false;
|
|
2491
|
+
// What is still typed is the person's too: read, a name looked up waited for, before theirs are loaded.
|
|
2492
|
+
const waiting = readTyped();
|
|
2493
|
+
if (waiting)
|
|
2494
|
+
await waiting;
|
|
2495
|
+
if (!asked())
|
|
2496
|
+
return false;
|
|
2497
|
+
// The person's changes, kept before theirs are loaded over them; and text they left that does not read.
|
|
2140
2498
|
const mine = {};
|
|
2141
2499
|
for (const name of state.dirty)
|
|
2142
2500
|
mine[name] = (0, values_1.structuredCopy)(state.values[name]);
|
|
2143
|
-
|
|
2144
|
-
|
|
2501
|
+
const typed = state.unread;
|
|
2502
|
+
// The record loaded again is a record shown anew: what is set aside is laid on it alone.
|
|
2503
|
+
const loading = load(true);
|
|
2504
|
+
const loaded = generation;
|
|
2505
|
+
await track(loading);
|
|
2506
|
+
if (loaded !== generation || state.status !== 'ready')
|
|
2145
2507
|
return false;
|
|
2146
2508
|
// Set aside where theirs differ from mine: a change they made the same way is no loss.
|
|
2147
2509
|
const aside = { ...(state.setAside ?? {}) };
|
|
2148
2510
|
for (const [name, value] of Object.entries(mine))
|
|
2149
2511
|
if (!sameValue(fields[name], value, state.values[name]))
|
|
2150
2512
|
aside[name] = value;
|
|
2151
|
-
set
|
|
2513
|
+
// Text that does not read is no value to set aside: it stays in its box, unread with why, over their value —
|
|
2514
|
+
// never dropped unsaid. A line of theirs no longer there takes its cells' text with it.
|
|
2515
|
+
const unread = Object.fromEntries(Object.entries(typed).filter(([path]) => valueAt(state.values, path) !== gone));
|
|
2516
|
+
set({ conflict: null, saveProblem: null, setAside: Object.keys(aside).length ? aside : null, ...(Object.keys(unread).length ? { unread } : {}) });
|
|
2152
2517
|
return true;
|
|
2153
2518
|
},
|
|
2154
2519
|
async keepMine() {
|
|
@@ -2159,6 +2524,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2159
2524
|
const source = options.dataSource;
|
|
2160
2525
|
if (conflict && !told) {
|
|
2161
2526
|
if (source?.load && state.recordId != null && page.data.kind === 'record') {
|
|
2527
|
+
const shown = shownNow();
|
|
2162
2528
|
try {
|
|
2163
2529
|
const now = (0, data_source_1.loadedRecordOf)(await track(source.load({ model: page.data.model, id: state.recordId, fields })));
|
|
2164
2530
|
told = now.values;
|
|
@@ -2167,6 +2533,9 @@ function innerForm(options, within, place, outer) {
|
|
|
2167
2533
|
catch {
|
|
2168
2534
|
// Unread: said below.
|
|
2169
2535
|
}
|
|
2536
|
+
// Read, or not, once another record is shown or the values let go: nothing is laid, nothing saved.
|
|
2537
|
+
if (!shown())
|
|
2538
|
+
return false;
|
|
2170
2539
|
}
|
|
2171
2540
|
// Still unread: saving now would go over work this person never saw. Nothing is saved; they are told, and choose again.
|
|
2172
2541
|
if (!told) {
|
|
@@ -2229,7 +2598,8 @@ function innerForm(options, within, place, outer) {
|
|
|
2229
2598
|
const given = state.values[part.name];
|
|
2230
2599
|
// A record of no model of its own: its links search and its lists load, but it never loads, saves, sends or recalculates alone.
|
|
2231
2600
|
const made = innerForm({
|
|
2232
|
-
|
|
2601
|
+
// In the record's time zone, unless it names its own.
|
|
2602
|
+
page: { ...inner, data: { kind: 'responses' }, ...(inner.timeZone || !page.timeZone ? {} : { timeZone: page.timeZone }) },
|
|
2233
2603
|
dataSource: options.dataSource,
|
|
2234
2604
|
values: isRecord(given) ? given : {},
|
|
2235
2605
|
onAction: options.onAction,
|
|
@@ -2242,7 +2612,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2242
2612
|
// Its files are the record's: stored for it, under the part's name.
|
|
2243
2613
|
{ model: where.model, recordId: where.recordId, under: `${where.under}${part.name}.` },
|
|
2244
2614
|
// Its ways out are the record's: through this form's check.
|
|
2245
|
-
{ letGo, close, leaveNow: (how) => atOnce(how) });
|
|
2615
|
+
{ letGo, close, leaveNow: (how) => atOnce(how), recordShown: shownNow });
|
|
2246
2616
|
attached.set(id, { name: part.name, inner: made });
|
|
2247
2617
|
// A change inside it is told as a change of its answers here, whoever made it.
|
|
2248
2618
|
made.form.on('change', ({ by }) => {
|
|
@@ -2486,6 +2856,12 @@ function indexLayout(root) {
|
|
|
2486
2856
|
else if (node.type === 'form') {
|
|
2487
2857
|
add(node, parent, 'form', { invisible: node.invisible, readonly: node.readonly });
|
|
2488
2858
|
}
|
|
2859
|
+
else if (node.type === 'related') {
|
|
2860
|
+
add(node, parent, 'other', { invisible: node.invisible });
|
|
2861
|
+
// A row's buttons: their roles here, hidden with the list, their condition read by it on each row's record.
|
|
2862
|
+
for (const button of node.rowButtons ?? [])
|
|
2863
|
+
add(button, node.id, 'button', {});
|
|
2864
|
+
}
|
|
2489
2865
|
else {
|
|
2490
2866
|
add(node, parent, 'other', { invisible: node.invisible });
|
|
2491
2867
|
}
|
|
@@ -2512,6 +2888,9 @@ function indexLayout(root) {
|
|
|
2512
2888
|
// A list holds no fields of its own record: only the buttons for what is selected.
|
|
2513
2889
|
for (const button of root.actions ?? [])
|
|
2514
2890
|
add(button, root.id, 'button', { invisible: button.invisible });
|
|
2891
|
+
// A row's buttons: their roles here, their condition read by the list on each row's record, which the form does not hold.
|
|
2892
|
+
for (const button of root.rowButtons ?? [])
|
|
2893
|
+
add(button, root.id, 'button', {});
|
|
2515
2894
|
}
|
|
2516
2895
|
else {
|
|
2517
2896
|
// The title and statusbar show fields too, so they are validated like
|