@fieldia/core 0.22.0 → 0.24.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 +135 -3
- package/package.json +1 -1
- package/schema/page.schema.json +159 -4
- package/src/index.d.mts +6 -2
- package/src/index.d.ts +6 -2
- package/src/index.js +18 -1
- package/src/index.js.map +1 -1
- package/src/index.mjs +8 -2
- package/src/index.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/field.d.mts +55 -34
- package/src/lib/format/field.d.ts +55 -34
- package/src/lib/format/field.js +16 -2
- package/src/lib/format/field.js.map +1 -1
- package/src/lib/format/field.mjs +15 -1
- package/src/lib/format/field.mjs.map +1 -1
- package/src/lib/format/layout.d.mts +365 -66
- package/src/lib/format/layout.d.ts +365 -66
- package/src/lib/format/layout.js +22 -4
- package/src/lib/format/layout.js.map +1 -1
- package/src/lib/format/layout.mjs +20 -3
- package/src/lib/format/layout.mjs.map +1 -1
- package/src/lib/format/page.d.mts +169 -29
- package/src/lib/format/page.d.ts +169 -29
- package/src/lib/format/page.js +11 -0
- package/src/lib/format/page.js.map +1 -1
- package/src/lib/format/page.mjs +12 -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 +10 -0
- package/src/lib/format/references.d.ts +10 -0
- package/src/lib/format/references.js +96 -1
- package/src/lib/format/references.js.map +1 -1
- package/src/lib/format/references.mjs +96 -1
- 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/upgrade.d.mts +4 -1
- package/src/lib/format/upgrade.d.ts +4 -1
- package/src/lib/format/upgrade.js +4 -1
- package/src/lib/format/upgrade.js.map +1 -1
- package/src/lib/format/upgrade.mjs +4 -1
- 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 +188 -3
- package/src/lib/record/data-source.d.ts +188 -3
- package/src/lib/record/data-source.js.map +1 -1
- package/src/lib/record/data-source.mjs.map +1 -1
- package/src/lib/record/form.d.mts +72 -25
- package/src/lib/record/form.d.ts +72 -25
- package/src/lib/record/form.js +404 -78
- package/src/lib/record/form.js.map +1 -1
- package/src/lib/record/form.mjs +404 -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 +13 -2
- package/src/lib/record/memory-data-source.d.ts +13 -2
- package/src/lib/record/memory-data-source.js +92 -35
- package/src/lib/record/memory-data-source.js.map +1 -1
- package/src/lib/record/memory-data-source.mjs +93 -36
- package/src/lib/record/memory-data-source.mjs.map +1 -1
- 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 +44 -8
- package/src/lib/record/run.d.ts +44 -8
- package/src/lib/record/run.js +155 -26
- package/src/lib/record/run.js.map +1 -1
- package/src/lib/record/run.mjs +156 -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/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 +12 -5
- package/src/runtime.d.ts +12 -5
- package/src/runtime.js +32 -2
- package/src/runtime.js.map +1 -1
- package/src/runtime.mjs +7 -1
- 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");
|
|
@@ -34,15 +35,23 @@ function unsavedWork(state) {
|
|
|
34
35
|
}
|
|
35
36
|
/**
|
|
36
37
|
* 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
|
-
*
|
|
38
|
+
* change of theirs the last save did not send, or text they left since that
|
|
39
|
+
* does not read ("12a" counts as "12" does: `unsent`), files still on
|
|
40
|
+
* their way, changes Reload set aside, the changes of a save refused for a
|
|
41
|
+
* conflict, waiting for Reload or Keep mine (only a source that keeps
|
|
42
|
+
* versions has one) — and, `how.changes`, any change not saved. What
|
|
43
|
+
* `form.leave()` — the one check every way out goes through — asks about,
|
|
44
|
+
* once the save on its way and the files on theirs settled. Before a
|
|
45
|
+
* record's first save, a change not saved is not at risk: as in 0.20, each
|
|
46
|
+
* way out decides.
|
|
43
47
|
*/
|
|
44
48
|
function workAtRisk(state, how = {}) {
|
|
45
|
-
return (state.unsent ?? []).length > 0 ||
|
|
49
|
+
return ((state.unsent ?? []).length > 0 ||
|
|
50
|
+
state.setAside != null ||
|
|
51
|
+
(state.uploads ?? []).some(uploads_1.uploadGoing) ||
|
|
52
|
+
// The person asked for these to be saved, and someone else's save came first: still theirs, and not saved.
|
|
53
|
+
(state.conflict != null && state.dirty.length > 0) ||
|
|
54
|
+
(how.changes === true && (state.dirty.length > 0 || Object.keys(state.unread ?? {}).length > 0)));
|
|
46
55
|
}
|
|
47
56
|
/**
|
|
48
57
|
* The record state behind a page: values, what changed, what is visible and
|
|
@@ -54,6 +63,8 @@ function createForm(options) {
|
|
|
54
63
|
const page = (0, upgrade_1.upgradePage)(options.page);
|
|
55
64
|
return innerForm(page === options.page ? options : { ...options, page }, []).form;
|
|
56
65
|
}
|
|
66
|
+
/** Whether a run works a person's change out: a page's `change` steps, set off by it, or a run they set off. */
|
|
67
|
+
const worksOut = (chain) => chain.some((key) => key.startsWith('on.change.'));
|
|
57
68
|
/** A line's cell whose line is no longer there. */
|
|
58
69
|
const gone = Symbol('gone');
|
|
59
70
|
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -66,7 +77,8 @@ const asValue = (values) => values;
|
|
|
66
77
|
* its ways out go through that one's check.
|
|
67
78
|
*/
|
|
68
79
|
function innerForm(options, within, place, outer) {
|
|
69
|
-
|
|
80
|
+
// Its time zone written into each date and time field naming none, for whatever reads one from its field alone.
|
|
81
|
+
const page = (0, zoned_1.zonedPage)(options.page);
|
|
70
82
|
const scheduler = options.scheduler ?? scheduler_1.hostScheduler;
|
|
71
83
|
const messages = options.messages ?? messages_1.MESSAGES.en;
|
|
72
84
|
const index = indexLayout(page.layout);
|
|
@@ -130,7 +142,19 @@ function innerForm(options, within, place, outer) {
|
|
|
130
142
|
const warns = [...nodeRules.values()].some((rules) => rules.some((compiled) => compiled.rule.level === 'warning'));
|
|
131
143
|
const steps = page.layout.type === 'wizard' ? page.layout.children.map((step) => step.id) : [];
|
|
132
144
|
const draftKey = () => `fieldia:draft:${page.id}:${state.recordId ?? 'new'}`;
|
|
133
|
-
|
|
145
|
+
/** Today, on the clock of the page's time zone (the browser's own when it names none). */
|
|
146
|
+
const today = () => (0, zones_1.dayIn)(options.now?.() ?? new Date(), page.timeZone);
|
|
147
|
+
const clock = () => (options.now?.() ?? new Date()).getTime();
|
|
148
|
+
/** What a page's bot check saw — its hidden box filled, the captcha's token — and when the page was shown or started again. */
|
|
149
|
+
let bot = { honeypot: false, captcha: null, since: clock() };
|
|
150
|
+
/** What the bot check saw as the answers go; none without one. */
|
|
151
|
+
function botReport() {
|
|
152
|
+
const check = page.botCheck;
|
|
153
|
+
if (!check)
|
|
154
|
+
return undefined;
|
|
155
|
+
const taken = Math.max(0, clock() - bot.since) / 1000;
|
|
156
|
+
return { honeypot: check.honeypot !== false && bot.honeypot, tooFast: taken < (check.minSeconds ?? 3), seconds: Math.round(taken * 10) / 10, ...(check.captcha ? { captcha: bot.captcha } : {}) };
|
|
157
|
+
}
|
|
134
158
|
// 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
159
|
let started = false;
|
|
136
160
|
let editing = options.editing !== false;
|
|
@@ -292,16 +316,41 @@ function innerForm(options, within, place, outer) {
|
|
|
292
316
|
let generation = 0;
|
|
293
317
|
/** Closed (`dispose`): a run still going stops quietly, as its dialog or page is gone — no other record replaced it. */
|
|
294
318
|
let disposed = false;
|
|
295
|
-
/**
|
|
319
|
+
/** Counted at each reset (the person's Discard): a save on its way then lands as the record it stored. */
|
|
320
|
+
let resets = 0;
|
|
321
|
+
/**
|
|
322
|
+
* THE test every late answer passes (`recordShown`): taken as it is asked for, true while the record it was asked
|
|
323
|
+
* 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
|
|
324
|
+
* dialog, a record step's — none is laid on another record, nor over the values the person's Discard let go.
|
|
325
|
+
*/
|
|
326
|
+
function shownNow() {
|
|
327
|
+
const at = generation;
|
|
328
|
+
const reset = resets;
|
|
329
|
+
// A saved form placed in a record is the record's: another shown round it, its answers are laid nowhere either.
|
|
330
|
+
const round = outer?.recordShown();
|
|
331
|
+
return () => !disposed && at === generation && reset === resets && (!round || round());
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* The person's claims on the record: the fields whose last write was theirs. A step's, a rule's or the app's
|
|
335
|
+
* write of the whole field takes one over; a write that works the person's change out (the source's onchange, the
|
|
336
|
+
* page's `change` steps it sets off) keeps it.
|
|
337
|
+
*/
|
|
296
338
|
const personal = new Set();
|
|
339
|
+
/**
|
|
340
|
+
* Of a table the person did not write whole: the keys of the lines they changed, added or took away. A line a
|
|
341
|
+
* step adds is none of theirs, and leaves theirs as they are.
|
|
342
|
+
*/
|
|
343
|
+
const personalLines = new Map();
|
|
297
344
|
/** Fields where a file the person picked landed from the store: at risk until a save sends it, a first save or not. */
|
|
298
345
|
const filed = new Set();
|
|
299
346
|
/** 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
347
|
let sentAs = null;
|
|
301
|
-
/**
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
348
|
+
/**
|
|
349
|
+
* The records being deleted, by the person's choice, each by its generation: a way out of one of them lets their
|
|
350
|
+
* work in it go, unasked. Another record shown meanwhile is asked about as any.
|
|
351
|
+
*/
|
|
352
|
+
const deleting = new Map();
|
|
353
|
+
const deletingShown = () => (deleting.get(generation) ?? 0) > 0;
|
|
305
354
|
/** Each field's last comparison with what was sent, so a value that did not move is not compared again. */
|
|
306
355
|
const compared = new Map();
|
|
307
356
|
/** Fields showing a file that did not reach the store: shown until it is sent again or taken away, whatever else changes. */
|
|
@@ -439,8 +488,9 @@ function innerForm(options, within, place, outer) {
|
|
|
439
488
|
const was = state.values;
|
|
440
489
|
const shown = state.errors;
|
|
441
490
|
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
|
-
|
|
491
|
+
// The person's changes the last save did not send, worked out again from the values as they are now, and from the
|
|
492
|
+
// text they left that does not read.
|
|
493
|
+
if (patch.unsent === undefined && (patch.values !== undefined || patch.unread !== undefined)) {
|
|
444
494
|
const unsent = unsentNow();
|
|
445
495
|
if (!sameList(unsent, state.unsent))
|
|
446
496
|
state = { ...state, unsent };
|
|
@@ -469,42 +519,66 @@ function innerForm(options, within, place, outer) {
|
|
|
469
519
|
for (const listener of [...heard])
|
|
470
520
|
listener(payload);
|
|
471
521
|
}
|
|
472
|
-
/**
|
|
473
|
-
|
|
474
|
-
|
|
522
|
+
/**
|
|
523
|
+
* A field written: told to the app, and — a person's change — the page's `change` steps for it run. `claim` says
|
|
524
|
+
* what of the person's it touches: `line`, the key of the one line of a table the person changed; `keep`, a write
|
|
525
|
+
* that leaves the person's claims as they are (a line a step adds, a write working their change out).
|
|
526
|
+
*/
|
|
527
|
+
function changed(field, by, claim = {}) {
|
|
528
|
+
noteOrigin(field, by, claim);
|
|
475
529
|
emit('change', { field, value: state.values[field], values: state.values, by });
|
|
476
530
|
const list = page.on?.change?.[field];
|
|
477
531
|
if (by === 'person' && list)
|
|
478
532
|
void track(moment(`on.change.${field}`, list, [], true));
|
|
479
533
|
}
|
|
480
|
-
/** Another record is shown: nothing of the last one's is the person's work any more. */
|
|
534
|
+
/** 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
535
|
function newGeneration() {
|
|
482
536
|
generation++;
|
|
483
537
|
personal.clear();
|
|
538
|
+
personalLines.clear();
|
|
484
539
|
filed.clear();
|
|
485
540
|
compared.clear();
|
|
486
541
|
sentAs = null;
|
|
542
|
+
clearFiles();
|
|
487
543
|
}
|
|
488
544
|
/** 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
545
|
const filedIn = (name) => filed.has(name) || [...attached.values()].some((part) => part.name === name && part.inner.files.filed());
|
|
490
546
|
/**
|
|
491
547
|
* The person's work no save took, by field, in the page's order: each of
|
|
492
548
|
* their changes that differs from the record as the last save left it —
|
|
493
|
-
*
|
|
549
|
+
* a field they wrote, or a line of a table they changed — and text they
|
|
550
|
+
* left in a box of it that does not read (`unread`), which no save can
|
|
551
|
+
* send; before a first save, only a file the store holds.
|
|
494
552
|
*/
|
|
495
553
|
function unsentNow() {
|
|
496
|
-
|
|
554
|
+
const typed = sentAs ? unreadFields() : null;
|
|
555
|
+
if (!personal.size && !personalLines.size && !typed?.size)
|
|
497
556
|
return [];
|
|
498
557
|
const out = [];
|
|
499
558
|
for (const name of Object.keys(fields)) {
|
|
500
|
-
|
|
559
|
+
// Typed and left unread since a save: the person's, as a change is — a value changed to "12" is, so is "12a".
|
|
560
|
+
if (typed?.has(name)) {
|
|
561
|
+
out.push(name);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const lines = personalLines.get(name);
|
|
565
|
+
if (!personal.has(name) && !lines)
|
|
501
566
|
continue;
|
|
502
567
|
const from = sentAs ?? (filedIn(name) ? baseline : null);
|
|
503
|
-
if (from
|
|
568
|
+
if (!from)
|
|
569
|
+
continue;
|
|
570
|
+
if (lines ? linesDiffer(name, lines, state.values[name], from[name]) : differs(name, state.values[name], from[name]))
|
|
504
571
|
out.push(name);
|
|
505
572
|
}
|
|
506
573
|
return out;
|
|
507
574
|
}
|
|
575
|
+
/** The fields with text left unread in a box of theirs: a field's, a line's cell's, a property's. */
|
|
576
|
+
function unreadFields() {
|
|
577
|
+
const out = new Set();
|
|
578
|
+
for (const path of Object.keys(state.unread ?? {}))
|
|
579
|
+
out.add(path.split('.')[0]);
|
|
580
|
+
return out;
|
|
581
|
+
}
|
|
508
582
|
function differs(name, value, sent) {
|
|
509
583
|
const known = compared.get(name);
|
|
510
584
|
if (known && known.value === value && known.sent === sent)
|
|
@@ -513,18 +587,53 @@ function innerForm(options, within, place, outer) {
|
|
|
513
587
|
compared.set(name, { value, sent, differs: verdict });
|
|
514
588
|
return verdict;
|
|
515
589
|
}
|
|
590
|
+
/** Whether any of these lines of a table differs from the table as sent: changed, added, or taken away. */
|
|
591
|
+
function linesDiffer(name, keys, value, sent) {
|
|
592
|
+
const def = fields[name];
|
|
593
|
+
const find = (lines, key) => (lines ?? []).find((line) => line.key === key);
|
|
594
|
+
for (const key of keys) {
|
|
595
|
+
const now = find(value, key);
|
|
596
|
+
const was = find(sent, key);
|
|
597
|
+
if (!now && !was)
|
|
598
|
+
continue;
|
|
599
|
+
if (!now || !was)
|
|
600
|
+
return true;
|
|
601
|
+
if (now.values === was.values)
|
|
602
|
+
continue;
|
|
603
|
+
for (const sub of new Set([...Object.keys(now.values), ...Object.keys(was.values)])) {
|
|
604
|
+
if (!sameValue(def.fields[sub], now.values[sub], was.values[sub]))
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
516
610
|
/** `unsent` as it is now, told when it changed. */
|
|
517
611
|
function refreshUnsent() {
|
|
518
612
|
const unsent = unsentNow();
|
|
519
613
|
if (!sameList(unsent, state.unsent))
|
|
520
614
|
set({ unsent });
|
|
521
615
|
}
|
|
522
|
-
/**
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
616
|
+
/**
|
|
617
|
+
* A field written, and by whom: the person's change is theirs — the field, or the line of a table they changed —
|
|
618
|
+
* until a step, a rule or the app writes the whole field in its place. A write that leaves their change where it
|
|
619
|
+
* is (`keep`: a line a step adds, a `change` step working out what they typed) leaves it theirs.
|
|
620
|
+
*/
|
|
621
|
+
function noteOrigin(field, by, claim) {
|
|
622
|
+
if (by === 'person') {
|
|
623
|
+
if (claim.line === undefined) {
|
|
624
|
+
personal.add(field);
|
|
625
|
+
personalLines.delete(field);
|
|
626
|
+
}
|
|
627
|
+
else if (!personal.has(field)) {
|
|
628
|
+
const keys = personalLines.get(field) ?? new Set();
|
|
629
|
+
keys.add(claim.line);
|
|
630
|
+
personalLines.set(field, keys);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else if (!claim.keep) {
|
|
527
634
|
personal.delete(field);
|
|
635
|
+
personalLines.delete(field);
|
|
636
|
+
}
|
|
528
637
|
refreshUnsent();
|
|
529
638
|
}
|
|
530
639
|
/** A wizard step entered: told to the app, and its `show` steps run. */
|
|
@@ -694,10 +803,12 @@ function innerForm(options, within, place, outer) {
|
|
|
694
803
|
const ticks = node.source.widget === 'tick' && page.fields[name].type === 'boolean';
|
|
695
804
|
return ticks && state.values[name] !== true && nodeState(node.id).required ? messages.tick : undefined;
|
|
696
805
|
}
|
|
806
|
+
/** The widget a field is shown with where it keeps its value its own way (a per cent): the first of its nodes'. */
|
|
807
|
+
const shownWidget = (shown) => shown.map((node) => node.source.widget).find((widget) => widget === 'percentage');
|
|
697
808
|
/** A field node's error now: its field's own rules first, then its answer rules. */
|
|
698
809
|
function nodeError(node) {
|
|
699
810
|
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;
|
|
811
|
+
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
812
|
}
|
|
702
813
|
/** Warnings for the visible fields of the page, without publishing them. */
|
|
703
814
|
function collectWarnings() {
|
|
@@ -781,7 +892,7 @@ function innerForm(options, within, place, outer) {
|
|
|
781
892
|
const cell = ruled[sub];
|
|
782
893
|
if (cell?.invisible || (tables.has(node.id) && tables.get(node.id).compiled.hidden(sub, reading().context, reading().env, userRoles)))
|
|
783
894
|
continue;
|
|
784
|
-
const lineMessage = (0, check_1.checkValue)(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today());
|
|
895
|
+
const lineMessage = (0, check_1.checkValue)(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today(), node.source.cells?.[sub]?.widget);
|
|
785
896
|
if (lineMessage)
|
|
786
897
|
errors[`${name}.${line.key}.${sub}`] = lineMessage;
|
|
787
898
|
}
|
|
@@ -846,7 +957,7 @@ function innerForm(options, within, place, outer) {
|
|
|
846
957
|
if ((0, values_1.isEmpty)(fieldDef(field), value))
|
|
847
958
|
return undefined;
|
|
848
959
|
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())) ??
|
|
960
|
+
besidesRows(field, (0, check_1.checkValue)(checked(field), value, false, messages, today(), shownWidget(shown))) ??
|
|
850
961
|
shown.map((node) => ruleCheck(node).error).find(Boolean));
|
|
851
962
|
}
|
|
852
963
|
if (parts.length === 2 && page.fields[field]?.type === 'properties')
|
|
@@ -1032,8 +1143,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1032
1143
|
return;
|
|
1033
1144
|
const seq = ++onchangeSeq;
|
|
1034
1145
|
const request = { model: page.data.model, id: state.recordId, changed, values: (0, values_1.structuredCopy)(state.values) };
|
|
1146
|
+
const shown = shownNow();
|
|
1035
1147
|
void track(source.onchange(request).then((result) => {
|
|
1036
|
-
|
|
1148
|
+
// A newer change asked since; or the record it was asked for is gone, or its values let go: none of it is laid.
|
|
1149
|
+
if (seq !== onchangeSeq || !shown())
|
|
1037
1150
|
return;
|
|
1038
1151
|
const values = { ...state.values, ...(result.values ?? {}) };
|
|
1039
1152
|
writeValues(values, { warning: result.warning ?? null, warningField: result.warning ? changed : null });
|
|
@@ -1134,18 +1247,27 @@ function innerForm(options, within, place, outer) {
|
|
|
1134
1247
|
set({ status: 'error', error: problem.message, saveProblem: problem, conflict: null });
|
|
1135
1248
|
return { reason: 'save', message: problem.message };
|
|
1136
1249
|
}
|
|
1250
|
+
// Asked for the record shown now: another shown while it waits below, it saves nothing of that one, nor says anything there.
|
|
1251
|
+
const asked = generation;
|
|
1252
|
+
const elsewhere = () => ({ reason: 'save', message: messages.recordReplaced });
|
|
1137
1253
|
// 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
1254
|
// up is waited for, so the record it names is what is saved — or, named by none, why nothing is.
|
|
1139
1255
|
const waiting = readTyped();
|
|
1140
1256
|
if (waiting)
|
|
1141
1257
|
await waiting;
|
|
1258
|
+
if (asked !== generation)
|
|
1259
|
+
return elsewhere();
|
|
1142
1260
|
// 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
1261
|
if (filesGoing()) {
|
|
1144
1262
|
set({ status: 'saving', error: null, saveProblem: null });
|
|
1145
1263
|
await filesSettled();
|
|
1264
|
+
if (asked !== generation)
|
|
1265
|
+
return elsewhere();
|
|
1146
1266
|
set({ status: 'ready' });
|
|
1147
1267
|
}
|
|
1148
1268
|
if (page.on?.beforeSave) {
|
|
1269
|
+
// Its run stops of itself on a record shown by anything but its own steps (a `call`'s `{ record }`, a reload):
|
|
1270
|
+
// those move the save on with it.
|
|
1149
1271
|
const before = await moment('on.beforeSave', page.on.beforeSave, chain, false);
|
|
1150
1272
|
if (!before.done) {
|
|
1151
1273
|
// A check's problems, or an inner save's, are shown already; any other stop is this save's problem.
|
|
@@ -1170,6 +1292,8 @@ function innerForm(options, within, place, outer) {
|
|
|
1170
1292
|
try {
|
|
1171
1293
|
// 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
1294
|
await storeBytes(page.data.kind === 'responses' || state.recordId == null ? Object.keys(fields) : state.dirty);
|
|
1295
|
+
if (at !== generation)
|
|
1296
|
+
return elsewhere();
|
|
1173
1297
|
const failed = fileProblems();
|
|
1174
1298
|
if (Object.keys(failed).length) {
|
|
1175
1299
|
set({ errors: failed, ...unstored(failed) });
|
|
@@ -1181,7 +1305,11 @@ function innerForm(options, within, place, outer) {
|
|
|
1181
1305
|
const sent = shownValues();
|
|
1182
1306
|
const before = state.values;
|
|
1183
1307
|
at = generation;
|
|
1184
|
-
|
|
1308
|
+
const seen = botReport();
|
|
1309
|
+
await source.submit({ pageId: page.id, values: sent, ...(seen ? { bot: seen } : {}) });
|
|
1310
|
+
// The captcha's token is spent once sent: the next send waits for a new one.
|
|
1311
|
+
if (seen?.captcha)
|
|
1312
|
+
bot.captcha = null;
|
|
1185
1313
|
// Answered for answers no longer shown — the form was started again meanwhile: the source has them, the form lays nothing.
|
|
1186
1314
|
if (at !== generation)
|
|
1187
1315
|
return null;
|
|
@@ -1189,7 +1317,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1189
1317
|
// What it sent is what an answer changed since is told against: still to send.
|
|
1190
1318
|
sentAs = before;
|
|
1191
1319
|
set({ status: 'saved', unsent: unsentNow() });
|
|
1192
|
-
emit('send', { values: sent });
|
|
1320
|
+
emit('send', { values: sent, ...(seen ? { bot: seen } : {}) });
|
|
1193
1321
|
void track(moment('on.afterSave', page.on?.afterSave, chain, false));
|
|
1194
1322
|
return null;
|
|
1195
1323
|
}
|
|
@@ -1225,7 +1353,8 @@ function innerForm(options, within, place, outer) {
|
|
|
1225
1353
|
scheduleDraft();
|
|
1226
1354
|
scheduleAutosave();
|
|
1227
1355
|
}
|
|
1228
|
-
|
|
1356
|
+
// The record as this save left it: a change made while it was on its way is no value of it.
|
|
1357
|
+
emit('save', { recordId: state.recordId, values: baseline });
|
|
1229
1358
|
void track(moment('on.afterSave', page.on?.afterSave, chain, false));
|
|
1230
1359
|
return null;
|
|
1231
1360
|
}
|
|
@@ -1281,8 +1410,12 @@ function innerForm(options, within, place, outer) {
|
|
|
1281
1410
|
}
|
|
1282
1411
|
/** The form has its values: told to the app, its `open` steps run, then the wizard step it shows is told. */
|
|
1283
1412
|
async function opening() {
|
|
1413
|
+
const at = generation;
|
|
1284
1414
|
emit('open', { recordId: state.recordId, values: state.values });
|
|
1285
1415
|
await moment('on.open', page.on?.open, [], true);
|
|
1416
|
+
// Another record shown while its open steps ran: that one's opening tells its own steps.
|
|
1417
|
+
if (at !== generation)
|
|
1418
|
+
return;
|
|
1286
1419
|
showing = true;
|
|
1287
1420
|
tellStep([]);
|
|
1288
1421
|
}
|
|
@@ -1303,8 +1436,8 @@ function innerForm(options, within, place, outer) {
|
|
|
1303
1436
|
for (const name of names)
|
|
1304
1437
|
changed(name, 'app');
|
|
1305
1438
|
}
|
|
1306
|
-
/** Write fields — a step's or the app's — worked out once, and tell each. */
|
|
1307
|
-
function write(written, by) {
|
|
1439
|
+
/** Write fields — a step's or the app's — worked out once, and tell each; `keep`, a write working the person's change out. */
|
|
1440
|
+
function write(written, by, keep = false) {
|
|
1308
1441
|
const names = Object.keys(written);
|
|
1309
1442
|
names.forEach(fieldDef);
|
|
1310
1443
|
if (!names.length)
|
|
@@ -1313,8 +1446,9 @@ function innerForm(options, within, place, outer) {
|
|
|
1313
1446
|
for (const name of names)
|
|
1314
1447
|
afterEdit(name);
|
|
1315
1448
|
for (const name of names)
|
|
1316
|
-
changed(name, by);
|
|
1449
|
+
changed(name, by, { keep });
|
|
1317
1450
|
}
|
|
1451
|
+
/** A line added: the person's, theirs; a step's, none of theirs, and the lines they changed stay theirs. */
|
|
1318
1452
|
function addLine(field, values, by) {
|
|
1319
1453
|
const def = lineField(field);
|
|
1320
1454
|
const current = state.values[field] ?? [];
|
|
@@ -1337,7 +1471,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1337
1471
|
const lines = [...current, line];
|
|
1338
1472
|
writeValues({ ...state.values, [field]: lines });
|
|
1339
1473
|
afterEdit(field);
|
|
1340
|
-
changed(field, by);
|
|
1474
|
+
changed(field, by, by === 'person' ? { line: key } : { keep: true });
|
|
1341
1475
|
return key;
|
|
1342
1476
|
}
|
|
1343
1477
|
/**
|
|
@@ -1346,15 +1480,44 @@ function innerForm(options, within, place, outer) {
|
|
|
1346
1480
|
*/
|
|
1347
1481
|
async function check(names) {
|
|
1348
1482
|
names?.forEach(fieldDef);
|
|
1483
|
+
const at = generation;
|
|
1349
1484
|
const waiting = readTyped();
|
|
1350
1485
|
if (waiting)
|
|
1351
1486
|
await waiting;
|
|
1487
|
+
// Another record shown while it waited: nothing found is shown on that one.
|
|
1488
|
+
if (at !== generation)
|
|
1489
|
+
return replaced();
|
|
1352
1490
|
const all = collectErrors();
|
|
1353
1491
|
const found = names ? Object.fromEntries(Object.entries(all).filter(([key]) => names.some((name) => key === name || key.startsWith(`${name}.`)))) : all;
|
|
1354
1492
|
set({ errors: found });
|
|
1355
1493
|
const first = Object.values(found)[0];
|
|
1356
1494
|
return first === undefined ? null : { reason: 'check', message: first };
|
|
1357
1495
|
}
|
|
1496
|
+
/**
|
|
1497
|
+
* What is typed read first, for a run the person or the app set off (`RunScope.typed`): every box read as a save
|
|
1498
|
+
* reads it, a name looked up waited for. Text left that does not read stops the run, said under its box as a check
|
|
1499
|
+
* says it — never a value under it handed on or worked from.
|
|
1500
|
+
*/
|
|
1501
|
+
async function typedFirst() {
|
|
1502
|
+
const at = generation;
|
|
1503
|
+
const waiting = readTyped();
|
|
1504
|
+
if (waiting)
|
|
1505
|
+
await waiting;
|
|
1506
|
+
// Another record shown while it waited: the run is not for that one.
|
|
1507
|
+
if (at !== generation)
|
|
1508
|
+
return replaced();
|
|
1509
|
+
const found = unreadProblems();
|
|
1510
|
+
const first = Object.values(found)[0];
|
|
1511
|
+
if (first === undefined)
|
|
1512
|
+
return null;
|
|
1513
|
+
set({ errors: { ...state.errors, ...found } });
|
|
1514
|
+
return { reason: 'check', message: first };
|
|
1515
|
+
}
|
|
1516
|
+
/** The problems of text left unread in boxes shown — this form's and its saved forms' placed here — by path. */
|
|
1517
|
+
function unreadProblems() {
|
|
1518
|
+
const inner = (path) => [...attached.values()].some((part) => path.startsWith(`${part.name}.`) && !!part.inner.form.getState().unread?.[path.slice(part.name.length + 1)]);
|
|
1519
|
+
return Object.fromEntries(Object.entries(collectErrors()).filter(([path]) => !!state.unread[path] || inner(path)));
|
|
1520
|
+
}
|
|
1358
1521
|
/** Go to a wizard's step for a run; undefined when the target is no step of it, but a tab. */
|
|
1359
1522
|
async function goToStep(target, chain) {
|
|
1360
1523
|
if (!steps.includes(target))
|
|
@@ -1362,16 +1525,19 @@ function innerForm(options, within, place, outer) {
|
|
|
1362
1525
|
if (!form.steps().includes(target))
|
|
1363
1526
|
return { reason: 'cannot', message: `The step "${target}" does not apply now` };
|
|
1364
1527
|
// A name still being looked up in a box on the way is waited for, as a save waits for it.
|
|
1528
|
+
const at = generation;
|
|
1365
1529
|
const waiting = readTyped();
|
|
1366
1530
|
if (waiting)
|
|
1367
1531
|
await waiting;
|
|
1532
|
+
if (at !== generation)
|
|
1533
|
+
return replaced();
|
|
1368
1534
|
let went = false;
|
|
1369
1535
|
inRun(chain, () => (went = form.goTo(target)));
|
|
1370
1536
|
return went ? null : { reason: 'check', message: Object.values(state.errors)[0] };
|
|
1371
1537
|
}
|
|
1372
1538
|
/** 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
1539
|
async function runTop(list, scope) {
|
|
1374
|
-
scope.generation
|
|
1540
|
+
scope.generation ??= generation;
|
|
1375
1541
|
const result = await runner.runSteps(list, scope);
|
|
1376
1542
|
emit('run', { id: scope.id, steps: list, result });
|
|
1377
1543
|
return result;
|
|
@@ -1389,7 +1555,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1389
1555
|
throw new Error(`"${name}" cannot make records: its data source has no create, or it is not a link`);
|
|
1390
1556
|
return track(source.create({ model, name: typed.trim(), ...(Object.keys(values).length ? { values } : {}) }));
|
|
1391
1557
|
}
|
|
1392
|
-
async function runSearch(def, name, ctx, query, limit, model, ids) {
|
|
1558
|
+
async function runSearch(def, name, ctx, query, limit, { model, ids, exact } = {}) {
|
|
1393
1559
|
// Formatted text names people and records in place (@mention, #4127): searched by the model they are of.
|
|
1394
1560
|
const byModel = def.type === 'reference' || def.type === 'json' || def.type === 'html';
|
|
1395
1561
|
if (def.type !== 'many2one' && def.type !== 'many2many' && !byModel) {
|
|
@@ -1404,7 +1570,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1404
1570
|
const own = byModel || !def.filter ? undefined : (0, filter_1.resolveFilter)(def.filter, ctx);
|
|
1405
1571
|
const filter = ids ? [...(own ?? []), { field: 'id', op: 'in', value: ids }] : own;
|
|
1406
1572
|
const relation = byModel ? model : def.relation;
|
|
1407
|
-
return track(source.search({ model: relation, query, ...(filter ? { filter } : {}), limit }));
|
|
1573
|
+
return track(source.search({ model: relation, query, ...(filter ? { filter } : {}), limit, ...(exact ? { exact: true } : {}) }));
|
|
1408
1574
|
}
|
|
1409
1575
|
function lineField(field) {
|
|
1410
1576
|
const def = fieldDef(field);
|
|
@@ -1505,7 +1671,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1505
1671
|
}
|
|
1506
1672
|
}
|
|
1507
1673
|
/** 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) => !
|
|
1674
|
+
const atOnce = (how, own = false) => !deletingShown() && !checking && !busy(own) && (how.discard === true || !workAtRisk(state, how));
|
|
1509
1675
|
/** The saves on their way, settled: what a load that is not a way out (a conflict's Reload) waits for. */
|
|
1510
1676
|
async function savesSettled() {
|
|
1511
1677
|
while (saveRunning)
|
|
@@ -1531,10 +1697,13 @@ function innerForm(options, within, place, outer) {
|
|
|
1531
1697
|
if (outer)
|
|
1532
1698
|
return outer.letGo(how, chain);
|
|
1533
1699
|
// A delete the person chose: their work goes with the record, unasked.
|
|
1534
|
-
if (
|
|
1700
|
+
if (deletingShown()) {
|
|
1535
1701
|
forget();
|
|
1536
1702
|
return true;
|
|
1537
1703
|
}
|
|
1704
|
+
// The person's own Discard stops the files on their way, never waits for them.
|
|
1705
|
+
if (how.discard)
|
|
1706
|
+
clearFiles();
|
|
1538
1707
|
const own = chain.includes('on.beforeSave');
|
|
1539
1708
|
if (atOnce(how, own)) {
|
|
1540
1709
|
if (how.discard)
|
|
@@ -1577,13 +1746,46 @@ function innerForm(options, within, place, outer) {
|
|
|
1577
1746
|
forget();
|
|
1578
1747
|
return yes;
|
|
1579
1748
|
}
|
|
1580
|
-
/**
|
|
1749
|
+
/**
|
|
1750
|
+
* Let go of the person's work at risk: files on their way stop, changes set aside go, a conflict's changes are
|
|
1751
|
+
* asked about no more, and the changes no save took go back to the record as the last save left it — so what a
|
|
1752
|
+
* dialog or a page in place hands back once it saved is the record as saved, never the change let go.
|
|
1753
|
+
*/
|
|
1581
1754
|
function forget() {
|
|
1755
|
+
const back = unsentLetGo();
|
|
1582
1756
|
clearFiles();
|
|
1583
1757
|
personal.clear();
|
|
1758
|
+
personalLines.clear();
|
|
1584
1759
|
filed.clear();
|
|
1585
|
-
if (
|
|
1586
|
-
|
|
1760
|
+
if (back)
|
|
1761
|
+
writeValues({ ...state.values, ...back });
|
|
1762
|
+
// Text left that does not read goes too: each box shows what its field holds.
|
|
1763
|
+
const unread = Object.keys(state.unread).length ? { unread: {} } : {};
|
|
1764
|
+
if (state.setAside !== null || state.unsent.length || state.conflict !== null || unread.unread)
|
|
1765
|
+
set({ setAside: null, unsent: [], conflict: null, ...unread });
|
|
1766
|
+
}
|
|
1767
|
+
/** The fields holding the person's changes no save took, as the record as last saved holds them: their lines alone, for a table. */
|
|
1768
|
+
function unsentLetGo() {
|
|
1769
|
+
if (!state.unsent.length)
|
|
1770
|
+
return null;
|
|
1771
|
+
const out = {};
|
|
1772
|
+
for (const name of state.unsent) {
|
|
1773
|
+
const from = sentAs ?? baseline;
|
|
1774
|
+
const keys = personalLines.get(name);
|
|
1775
|
+
out[name] = keys && !personal.has(name) ? linesAsSent(state.values[name], from[name], keys) : (0, values_1.structuredCopy)(from[name]);
|
|
1776
|
+
}
|
|
1777
|
+
return out;
|
|
1778
|
+
}
|
|
1779
|
+
/** A table with these lines as sent: changed ones as they were, added ones gone, ones taken away back in their place. */
|
|
1780
|
+
function linesAsSent(value, sent, keys) {
|
|
1781
|
+
const was = sent ?? [];
|
|
1782
|
+
const of = (key) => was.find((line) => line.key === key);
|
|
1783
|
+
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));
|
|
1784
|
+
was.forEach((line, at) => {
|
|
1785
|
+
if (keys.has(line.key) && !out.some((other) => other.key === line.key))
|
|
1786
|
+
out.splice(Math.min(at, out.length), 0, (0, values_1.structuredCopy)(line));
|
|
1787
|
+
});
|
|
1788
|
+
return out;
|
|
1587
1789
|
}
|
|
1588
1790
|
/** The check as a step meets it: null to go on — at once when nothing is at risk — else why the run stops. */
|
|
1589
1791
|
function letGoStep(chain) {
|
|
@@ -1591,12 +1793,19 @@ function innerForm(options, within, place, outer) {
|
|
|
1591
1793
|
return go === true ? null : go.then((yes) => (yes ? null : kept()));
|
|
1592
1794
|
}
|
|
1593
1795
|
const kept = () => ({ reason: 'no', message: messages.discardChanges });
|
|
1594
|
-
/**
|
|
1595
|
-
|
|
1796
|
+
/**
|
|
1797
|
+
* A step's own move — the record loaded again, or another shown — once the check let what is shown go: kept, the
|
|
1798
|
+
* person kept their work; replaced, another record was shown before it ended (the run's steps are not for that one).
|
|
1799
|
+
*/
|
|
1800
|
+
async function ownMove(chain, move) {
|
|
1596
1801
|
const go = letGo({}, chain);
|
|
1597
|
-
if (go
|
|
1598
|
-
return
|
|
1599
|
-
|
|
1802
|
+
if (go !== true && !(await go))
|
|
1803
|
+
return kept();
|
|
1804
|
+
// The record it shows is a generation of its own, made as it starts: another made before it ends replaced it.
|
|
1805
|
+
const moving = move();
|
|
1806
|
+
const mine = generation;
|
|
1807
|
+
await moving;
|
|
1808
|
+
return mine === generation ? null : replaced();
|
|
1600
1809
|
}
|
|
1601
1810
|
/**
|
|
1602
1811
|
* The host's close, as every `close` step reaches it: the form takes it
|
|
@@ -1618,6 +1827,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1618
1827
|
const go = letGo({}, chain);
|
|
1619
1828
|
return go === true ? shut() : go.then((yes) => (yes ? shut() : kept()));
|
|
1620
1829
|
}
|
|
1830
|
+
/** Why a run stops whose record was replaced while it went on — quietly, when the form was closed. */
|
|
1831
|
+
function replaced() {
|
|
1832
|
+
return disposed ? { reason: 'closed' } : { reason: 'cannot', message: messages.recordReplaced };
|
|
1833
|
+
}
|
|
1621
1834
|
/** The record a record step asks the data source about. */
|
|
1622
1835
|
const record = () => ({ model: page.data.model, id: state.recordId, fields });
|
|
1623
1836
|
async function post(message) {
|
|
@@ -1640,18 +1853,30 @@ function innerForm(options, within, place, outer) {
|
|
|
1640
1853
|
recordId: () => state.recordId,
|
|
1641
1854
|
reading,
|
|
1642
1855
|
today,
|
|
1643
|
-
|
|
1856
|
+
// A step's write of a field takes the person's change in it over — unless the step works that change out: the
|
|
1857
|
+
// page's `change` steps, set off by it, as the source's onchange does.
|
|
1858
|
+
write: (values, chain) => write(values, 'step', worksOut(chain)),
|
|
1644
1859
|
store: (values) => store(values),
|
|
1860
|
+
// The person's own change: the field's, or a line of it (a claim by line): put back, the whole field is theirs
|
|
1861
|
+
// again — at risk, asked about — never less than it was.
|
|
1862
|
+
mine: (field) => personal.has(field) || personalLines.has(field),
|
|
1863
|
+
putBack(values, mine) {
|
|
1864
|
+
write(values, 'step');
|
|
1865
|
+
for (const name of mine)
|
|
1866
|
+
noteOrigin(name, 'person', {});
|
|
1867
|
+
},
|
|
1645
1868
|
addLine: (field, values) => void addLine(field, values, 'step'),
|
|
1646
1869
|
check,
|
|
1870
|
+
typedFirst,
|
|
1647
1871
|
save: saving,
|
|
1648
1872
|
generation: () => generation,
|
|
1649
|
-
gone:
|
|
1873
|
+
gone: replaced,
|
|
1650
1874
|
async reload(chain) {
|
|
1651
1875
|
if (state.recordId == null)
|
|
1652
1876
|
return { reason: 'cannot', message: 'The record is not saved yet: there is nothing to load again' };
|
|
1653
|
-
|
|
1654
|
-
|
|
1877
|
+
const stopped = await ownMove(chain, () => load(true));
|
|
1878
|
+
if (stopped)
|
|
1879
|
+
return stopped;
|
|
1655
1880
|
return state.status === 'error' ? { reason: 'cannot', message: state.error ?? 'The record could not be loaded again' } : null;
|
|
1656
1881
|
},
|
|
1657
1882
|
reset(chain) {
|
|
@@ -1672,24 +1897,32 @@ function innerForm(options, within, place, outer) {
|
|
|
1672
1897
|
changed: () => state.dirty.length > 0 || state.uploads.some(uploads_1.uploadGoing),
|
|
1673
1898
|
letGo: letGoStep,
|
|
1674
1899
|
async deleting(remove, done) {
|
|
1675
|
-
|
|
1900
|
+
const of = generation;
|
|
1901
|
+
const shown = shownNow();
|
|
1902
|
+
deleting.set(of, (deleting.get(of) ?? 0) + 1);
|
|
1676
1903
|
try {
|
|
1677
1904
|
const out = await remove();
|
|
1678
|
-
// Deleted: what the person had not saved in it goes with it. Refused, it stays theirs.
|
|
1679
|
-
|
|
1905
|
+
// Deleted: what the person had not saved in it goes with it. Refused, it stays theirs. Another record shown
|
|
1906
|
+
// meanwhile keeps what was typed in it.
|
|
1907
|
+
if (done(out) && shown())
|
|
1680
1908
|
forget();
|
|
1681
1909
|
return out;
|
|
1682
1910
|
}
|
|
1683
1911
|
finally {
|
|
1684
|
-
|
|
1912
|
+
const left = (deleting.get(of) ?? 1) - 1;
|
|
1913
|
+
if (left)
|
|
1914
|
+
deleting.set(of, left);
|
|
1915
|
+
else
|
|
1916
|
+
deleting.delete(of);
|
|
1685
1917
|
}
|
|
1686
1918
|
},
|
|
1687
1919
|
...(options.dataSource?.archive && page.data.kind === 'record' ? { archive: (archive) => track((options.dataSource?.archive)({ ...record(), archive })) } : {}),
|
|
1688
1920
|
...(options.dataSource?.copy && page.data.kind === 'record' ? { copy: async () => (await track((options.dataSource?.copy)(record()))).id } : {}),
|
|
1689
1921
|
...(options.dataSource?.delete && page.data.kind === 'record' ? { remove: () => track((options.dataSource?.delete)(record())) } : {}),
|
|
1690
1922
|
async showRecord(id, chain) {
|
|
1691
|
-
|
|
1692
|
-
|
|
1923
|
+
const stopped = await ownMove(chain, () => showNow(id));
|
|
1924
|
+
if (stopped)
|
|
1925
|
+
return stopped;
|
|
1693
1926
|
return state.status === 'error' ? { reason: 'cannot', message: state.error ?? 'The record could not be loaded' } : null;
|
|
1694
1927
|
},
|
|
1695
1928
|
told(event, id) {
|
|
@@ -1708,8 +1941,24 @@ function innerForm(options, within, place, outer) {
|
|
|
1708
1941
|
wordsOf: (field) => wordsOfValue(fieldDef(field), state.values[field]),
|
|
1709
1942
|
barred: (right) => (state.can?.[right] === false ? state.can.reason ?? messages[MAY_NOT[right]] : null),
|
|
1710
1943
|
});
|
|
1711
|
-
|
|
1712
|
-
|
|
1944
|
+
// 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
|
|
1945
|
+
// go when another record is shown by then (its steps were for that one).
|
|
1946
|
+
const moment = (0, run_1.createMoments)((key, list, chain, on) => runTop(list, { id: key, chain, generation: on }), () => generation);
|
|
1947
|
+
/**
|
|
1948
|
+
* Whether steps do something before their first save — set a status, call the app, open a page — so a record's
|
|
1949
|
+
* button made of them saves first. Steps that start with the save (Save and close), or only check before it, need
|
|
1950
|
+
* no save before them: theirs is the first.
|
|
1951
|
+
*/
|
|
1952
|
+
function actsBeforeSaving(list) {
|
|
1953
|
+
const first = list.findIndex((step) => step.do === 'save');
|
|
1954
|
+
return first > 0 && list.slice(0, first).some((step) => step.do !== 'check');
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* A press of a button: its confirmation first, then its steps and its app action as a final call, in `scope`. A
|
|
1958
|
+
* record's button whose steps act and then save saves the record first when it is new or changed, as Flectra's
|
|
1959
|
+
* button does: refused — a field it requires left empty — none of its steps run, so nothing it does happens to a
|
|
1960
|
+
* record that cannot be saved.
|
|
1961
|
+
*/
|
|
1713
1962
|
function press(source, scope) {
|
|
1714
1963
|
const list = [...(source.steps ?? [])];
|
|
1715
1964
|
if (source.action !== undefined)
|
|
@@ -1720,12 +1969,37 @@ function innerForm(options, within, place, outer) {
|
|
|
1720
1969
|
list.push({ do: builtin });
|
|
1721
1970
|
const asked = builtin === 'archive' ? messages.archiveConfirm : builtin === 'delete' ? messages.deleteConfirm : undefined;
|
|
1722
1971
|
const confirm = 'confirm' in source && source.confirm !== undefined ? source.confirm : asked;
|
|
1972
|
+
// On the record it was pressed on: another shown while its question is asked, it does nothing to that one.
|
|
1973
|
+
scope.generation ??= generation;
|
|
1974
|
+
// Pressed by the person: what is typed is read before a step reads the form or hands its values on.
|
|
1975
|
+
scope.typed ??= 'first';
|
|
1723
1976
|
return track((async () => {
|
|
1724
1977
|
if (confirm && !(await runner.ask(confirm))) {
|
|
1725
1978
|
const result = { done: false, reason: 'no', message: confirm };
|
|
1726
1979
|
emit('run', { id: scope.id, steps: list, result });
|
|
1727
1980
|
return result;
|
|
1728
1981
|
}
|
|
1982
|
+
const saveFirst = page.data.kind === 'record' && actsBeforeSaving(list);
|
|
1983
|
+
// What is typed is read first, as the run reads it before its first step that reads the form (run.ts): text
|
|
1984
|
+
// that does not read stops it with `check`, said under its box — before the save that comes first, and any step.
|
|
1985
|
+
if (saveFirst && scope.typed === 'first') {
|
|
1986
|
+
const unread = await typedFirst();
|
|
1987
|
+
if (unread) {
|
|
1988
|
+
const at = Math.max(0, list.findIndex((step) => step.do !== 'check'));
|
|
1989
|
+
const result = { done: false, stoppedAt: at, step: list[at], reason: unread.reason, ...(unread.message !== undefined ? { message: unread.message } : {}) };
|
|
1990
|
+
emit('run', { id: scope.id, steps: list, result });
|
|
1991
|
+
return result;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
// Then the save first, then the steps.
|
|
1995
|
+
if (saveFirst && (state.recordId == null || state.dirty.length > 0 || Object.keys(state.unread ?? {}).length > 0 || state.uploads.some(uploads_1.uploadGoing))) {
|
|
1996
|
+
const first = await saving(scope.chain);
|
|
1997
|
+
if (first) {
|
|
1998
|
+
const result = { done: false, step: { do: 'save' }, reason: first.reason, ...(first.message !== undefined ? { message: first.message } : {}), ...(first.error !== undefined ? { error: first.error } : {}) };
|
|
1999
|
+
emit('run', { id: scope.id, steps: list, result });
|
|
2000
|
+
return result;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
1729
2003
|
return runTop(list, scope);
|
|
1730
2004
|
})());
|
|
1731
2005
|
}
|
|
@@ -1751,6 +2025,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1751
2025
|
openRecord: (id) => track(openRecord(id)),
|
|
1752
2026
|
leave: (how) => Promise.resolve(letGo(how)),
|
|
1753
2027
|
leaveNow: (how = {}) => (outer ? outer.leaveNow(how) : atOnce(how)),
|
|
2028
|
+
recordShown: shownNow,
|
|
1754
2029
|
post,
|
|
1755
2030
|
setEditing(next) {
|
|
1756
2031
|
if (next === editing)
|
|
@@ -1779,7 +2054,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1779
2054
|
}
|
|
1780
2055
|
const own = shown.map(unticked).find(Boolean) ??
|
|
1781
2056
|
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());
|
|
2057
|
+
(0, check_1.checkValue)(checked(name), state.values[name], shown.some((node) => nodeState(node.id).required), messages, today(), shownWidget(shown));
|
|
1783
2058
|
return own ?? shown.map((node) => ruleCheck(node).error).find((error) => error !== undefined) ?? null;
|
|
1784
2059
|
},
|
|
1785
2060
|
setUnread(path, unread) {
|
|
@@ -1825,6 +2100,12 @@ function innerForm(options, within, place, outer) {
|
|
|
1825
2100
|
};
|
|
1826
2101
|
},
|
|
1827
2102
|
settle: () => readTyped() ?? Promise.resolve(),
|
|
2103
|
+
botCheck(seen) {
|
|
2104
|
+
if (seen.honeypot !== undefined)
|
|
2105
|
+
bot.honeypot = seen.honeypot;
|
|
2106
|
+
if (seen.captcha !== undefined)
|
|
2107
|
+
bot.captcha = seen.captcha;
|
|
2108
|
+
},
|
|
1828
2109
|
reading: () => readingNow > 0 || [...attached.values()].some((part) => part.inner.form.reading()),
|
|
1829
2110
|
validate() {
|
|
1830
2111
|
readTyped();
|
|
@@ -1834,6 +2115,27 @@ function innerForm(options, within, place, outer) {
|
|
|
1834
2115
|
},
|
|
1835
2116
|
save: () => track(save()),
|
|
1836
2117
|
canUpload: () => typeof options.dataSource?.upload === 'function',
|
|
2118
|
+
today,
|
|
2119
|
+
canFindSlots: () => typeof options.dataSource?.slots === 'function',
|
|
2120
|
+
findSlots(field, ask) {
|
|
2121
|
+
const source = options.dataSource;
|
|
2122
|
+
if (!source?.slots)
|
|
2123
|
+
return Promise.reject(new Error('The data source has no slots()'));
|
|
2124
|
+
const def = page.fields[field];
|
|
2125
|
+
const { schedule = field, length, between, from, to } = ask;
|
|
2126
|
+
return source.slots({
|
|
2127
|
+
model: page.data.kind === 'record' ? page.data.model : '',
|
|
2128
|
+
field,
|
|
2129
|
+
recordId: state.recordId,
|
|
2130
|
+
schedule,
|
|
2131
|
+
from,
|
|
2132
|
+
to,
|
|
2133
|
+
timeZone: (def?.type === 'datetime' && def.timeZone) || page.timeZone || (0, zones_1.ownZone)(),
|
|
2134
|
+
...(length ? { length } : {}),
|
|
2135
|
+
...(between ? { between } : {}),
|
|
2136
|
+
values: (0, values_1.structuredCopy)(state.values),
|
|
2137
|
+
});
|
|
2138
|
+
},
|
|
1837
2139
|
upload: (field, files, how = {}) => uploads.start(field, files, how),
|
|
1838
2140
|
uploading: (field) => state.uploads.filter((upload) => upload.field === field && !upload.line),
|
|
1839
2141
|
cancelUpload(id) {
|
|
@@ -1859,8 +2161,11 @@ function innerForm(options, within, place, outer) {
|
|
|
1859
2161
|
// 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
2162
|
resets++;
|
|
1861
2163
|
personal.clear();
|
|
2164
|
+
personalLines.clear();
|
|
1862
2165
|
filed.clear();
|
|
1863
2166
|
sentAs = null;
|
|
2167
|
+
// Started again: a new response, timed from now.
|
|
2168
|
+
bot = { ...bot, honeypot: false, since: clock() };
|
|
1864
2169
|
const values = syncParts((0, values_1.structuredCopy)(baseline), 'start');
|
|
1865
2170
|
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
2171
|
},
|
|
@@ -1873,7 +2178,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1873
2178
|
const lines = (state.values[field] ?? []).map((line) => line.key === key ? { ...line, values: { ...line.values, [name]: (0, values_1.structuredCopy)(value) } } : line);
|
|
1874
2179
|
writeValues({ ...state.values, [field]: lines });
|
|
1875
2180
|
afterEdit(field);
|
|
1876
|
-
changed(field, 'person');
|
|
2181
|
+
changed(field, 'person', { line: key });
|
|
1877
2182
|
},
|
|
1878
2183
|
moveLine(field, key, to) {
|
|
1879
2184
|
const def = lineField(field);
|
|
@@ -1918,10 +2223,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1918
2223
|
const lines = (state.values[field] ?? []).filter((line) => line.key !== key);
|
|
1919
2224
|
writeValues({ ...state.values, [field]: lines });
|
|
1920
2225
|
afterEdit(field);
|
|
1921
|
-
changed(field, 'person');
|
|
2226
|
+
changed(field, 'person', { line: key });
|
|
1922
2227
|
},
|
|
1923
2228
|
async search(field, query, limit = 8, options = {}) {
|
|
1924
|
-
return runSearch(fieldDef(field), field, context(), query, limit, options
|
|
2229
|
+
return runSearch(fieldDef(field), field, context(), query, limit, options);
|
|
1925
2230
|
},
|
|
1926
2231
|
async previewLine(field, key, values) {
|
|
1927
2232
|
lineField(field);
|
|
@@ -1946,7 +2251,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1946
2251
|
const line = (state.values[field] ?? []).find((l) => l.key === key);
|
|
1947
2252
|
if (!line)
|
|
1948
2253
|
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
|
|
2254
|
+
return runSearch(sub, subfield, (0, env_1.lineContext)(line.values, def.fields, context(), line.id ?? null), query, limit, options);
|
|
1950
2255
|
},
|
|
1951
2256
|
canCreate: (field) => creatable(page.fields[field]) !== null,
|
|
1952
2257
|
quickCreate: (field, typed) => runCreate(page.fields[field], field, typed, page.fields[field] ? createValuesOf(field) : {}),
|
|
@@ -2108,7 +2413,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2108
2413
|
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
2414
|
});
|
|
2110
2415
|
},
|
|
2111
|
-
run: (list, context = {}) => track(runTop(list, { id: context.id ?? 'run', chain: [], ...(context.recordIds ? { recordIds: [...context.recordIds] } : {}) })),
|
|
2416
|
+
run: (list, context = {}) => track(runTop(list, { id: context.id ?? 'run', chain: [], typed: 'first', ...(context.recordIds ? { recordIds: [...context.recordIds] } : {}) })),
|
|
2112
2417
|
shown(tab) {
|
|
2113
2418
|
// A wizard's steps are told by the form itself, as they are entered.
|
|
2114
2419
|
if (steps.includes(tab))
|
|
@@ -2135,20 +2440,36 @@ function innerForm(options, within, place, outer) {
|
|
|
2135
2440
|
},
|
|
2136
2441
|
async reloadTheirs() {
|
|
2137
2442
|
// A save on its way first: its answer is not dropped by the load.
|
|
2443
|
+
const asked = shownNow();
|
|
2138
2444
|
await savesSettled();
|
|
2139
|
-
|
|
2445
|
+
if (!asked())
|
|
2446
|
+
return false;
|
|
2447
|
+
// What is still typed is the person's too: read, a name looked up waited for, before theirs are loaded.
|
|
2448
|
+
const waiting = readTyped();
|
|
2449
|
+
if (waiting)
|
|
2450
|
+
await waiting;
|
|
2451
|
+
if (!asked())
|
|
2452
|
+
return false;
|
|
2453
|
+
// The person's changes, kept before theirs are loaded over them; and text they left that does not read.
|
|
2140
2454
|
const mine = {};
|
|
2141
2455
|
for (const name of state.dirty)
|
|
2142
2456
|
mine[name] = (0, values_1.structuredCopy)(state.values[name]);
|
|
2143
|
-
|
|
2144
|
-
|
|
2457
|
+
const typed = state.unread;
|
|
2458
|
+
// The record loaded again is a record shown anew: what is set aside is laid on it alone.
|
|
2459
|
+
const loading = load(true);
|
|
2460
|
+
const loaded = generation;
|
|
2461
|
+
await track(loading);
|
|
2462
|
+
if (loaded !== generation || state.status !== 'ready')
|
|
2145
2463
|
return false;
|
|
2146
2464
|
// Set aside where theirs differ from mine: a change they made the same way is no loss.
|
|
2147
2465
|
const aside = { ...(state.setAside ?? {}) };
|
|
2148
2466
|
for (const [name, value] of Object.entries(mine))
|
|
2149
2467
|
if (!sameValue(fields[name], value, state.values[name]))
|
|
2150
2468
|
aside[name] = value;
|
|
2151
|
-
set
|
|
2469
|
+
// Text that does not read is no value to set aside: it stays in its box, unread with why, over their value —
|
|
2470
|
+
// never dropped unsaid. A line of theirs no longer there takes its cells' text with it.
|
|
2471
|
+
const unread = Object.fromEntries(Object.entries(typed).filter(([path]) => valueAt(state.values, path) !== gone));
|
|
2472
|
+
set({ conflict: null, saveProblem: null, setAside: Object.keys(aside).length ? aside : null, ...(Object.keys(unread).length ? { unread } : {}) });
|
|
2152
2473
|
return true;
|
|
2153
2474
|
},
|
|
2154
2475
|
async keepMine() {
|
|
@@ -2159,6 +2480,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2159
2480
|
const source = options.dataSource;
|
|
2160
2481
|
if (conflict && !told) {
|
|
2161
2482
|
if (source?.load && state.recordId != null && page.data.kind === 'record') {
|
|
2483
|
+
const shown = shownNow();
|
|
2162
2484
|
try {
|
|
2163
2485
|
const now = (0, data_source_1.loadedRecordOf)(await track(source.load({ model: page.data.model, id: state.recordId, fields })));
|
|
2164
2486
|
told = now.values;
|
|
@@ -2167,6 +2489,9 @@ function innerForm(options, within, place, outer) {
|
|
|
2167
2489
|
catch {
|
|
2168
2490
|
// Unread: said below.
|
|
2169
2491
|
}
|
|
2492
|
+
// Read, or not, once another record is shown or the values let go: nothing is laid, nothing saved.
|
|
2493
|
+
if (!shown())
|
|
2494
|
+
return false;
|
|
2170
2495
|
}
|
|
2171
2496
|
// Still unread: saving now would go over work this person never saw. Nothing is saved; they are told, and choose again.
|
|
2172
2497
|
if (!told) {
|
|
@@ -2229,7 +2554,8 @@ function innerForm(options, within, place, outer) {
|
|
|
2229
2554
|
const given = state.values[part.name];
|
|
2230
2555
|
// 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
2556
|
const made = innerForm({
|
|
2232
|
-
|
|
2557
|
+
// In the record's time zone, unless it names its own.
|
|
2558
|
+
page: { ...inner, data: { kind: 'responses' }, ...(inner.timeZone || !page.timeZone ? {} : { timeZone: page.timeZone }) },
|
|
2233
2559
|
dataSource: options.dataSource,
|
|
2234
2560
|
values: isRecord(given) ? given : {},
|
|
2235
2561
|
onAction: options.onAction,
|
|
@@ -2242,7 +2568,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2242
2568
|
// Its files are the record's: stored for it, under the part's name.
|
|
2243
2569
|
{ model: where.model, recordId: where.recordId, under: `${where.under}${part.name}.` },
|
|
2244
2570
|
// Its ways out are the record's: through this form's check.
|
|
2245
|
-
{ letGo, close, leaveNow: (how) => atOnce(how) });
|
|
2571
|
+
{ letGo, close, leaveNow: (how) => atOnce(how), recordShown: shownNow });
|
|
2246
2572
|
attached.set(id, { name: part.name, inner: made });
|
|
2247
2573
|
// A change inside it is told as a change of its answers here, whoever made it.
|
|
2248
2574
|
made.form.on('change', ({ by }) => {
|