@fieldia/core 0.24.0 → 0.26.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 +133 -1
- package/package.json +1 -1
- package/schema/page.schema.json +983 -329
- package/src/index.d.mts +6 -2
- package/src/index.d.ts +6 -2
- package/src/index.js +16 -1
- package/src/index.js.map +1 -1
- package/src/index.mjs +7 -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/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 +236 -0
- package/src/lib/format/field.d.ts +236 -0
- package/src/lib/format/field.js +4 -3
- package/src/lib/format/field.js.map +1 -1
- package/src/lib/format/field.mjs +2 -1
- package/src/lib/format/field.mjs.map +1 -1
- package/src/lib/format/head-checks.d.mts +3 -1
- package/src/lib/format/head-checks.d.ts +3 -1
- package/src/lib/format/head-checks.js +25 -4
- package/src/lib/format/head-checks.js.map +1 -1
- package/src/lib/format/head-checks.mjs +24 -4
- package/src/lib/format/head-checks.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 +7391 -3264
- package/src/lib/format/layout.d.ts +7391 -3264
- package/src/lib/format/layout.js +100 -37
- package/src/lib/format/layout.js.map +1 -1
- package/src/lib/format/layout.mjs +99 -36
- 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 +1018 -118
- package/src/lib/format/page.d.ts +1018 -118
- package/src/lib/format/page.js +17 -0
- package/src/lib/format/page.js.map +1 -1
- package/src/lib/format/page.mjs +17 -0
- package/src/lib/format/page.mjs.map +1 -1
- package/src/lib/format/records.d.mts +23 -0
- package/src/lib/format/records.d.ts +23 -0
- package/src/lib/format/records.js +28 -0
- package/src/lib/format/records.js.map +1 -0
- package/src/lib/format/records.mjs +25 -0
- package/src/lib/format/records.mjs.map +1 -0
- package/src/lib/format/references.d.mts +31 -0
- package/src/lib/format/references.d.ts +31 -0
- package/src/lib/format/references.js +185 -24
- package/src/lib/format/references.js.map +1 -1
- package/src/lib/format/references.mjs +185 -24
- package/src/lib/format/references.mjs.map +1 -1
- package/src/lib/format/translate.js +12 -6
- package/src/lib/format/translate.js.map +1 -1
- package/src/lib/format/translate.mjs +12 -6
- 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/record/check.js +2 -2
- package/src/lib/record/check.js.map +1 -1
- package/src/lib/record/check.mjs +3 -3
- package/src/lib/record/check.mjs.map +1 -1
- package/src/lib/record/data-source.d.mts +121 -0
- package/src/lib/record/data-source.d.ts +121 -0
- 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 +51 -3
- package/src/lib/record/form.d.ts +51 -3
- package/src/lib/record/form.js +78 -17
- package/src/lib/record/form.js.map +1 -1
- package/src/lib/record/form.mjs +79 -18
- package/src/lib/record/form.mjs.map +1 -1
- package/src/lib/record/inputs.js +8 -8
- package/src/lib/record/inputs.js.map +1 -1
- package/src/lib/record/inputs.mjs +9 -9
- package/src/lib/record/inputs.mjs.map +1 -1
- package/src/lib/record/limits.js +1 -1
- package/src/lib/record/limits.js.map +1 -1
- package/src/lib/record/limits.mjs +2 -2
- package/src/lib/record/limits.mjs.map +1 -1
- package/src/lib/record/locales/de.js +2 -0
- package/src/lib/record/locales/de.js.map +1 -1
- package/src/lib/record/locales/de.mjs +2 -0
- package/src/lib/record/locales/de.mjs.map +1 -1
- package/src/lib/record/memory-data-source.d.mts +12 -5
- package/src/lib/record/memory-data-source.d.ts +12 -5
- package/src/lib/record/memory-data-source.js +85 -1
- package/src/lib/record/memory-data-source.js.map +1 -1
- package/src/lib/record/memory-data-source.mjs +86 -2
- 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/messages.d.mts +24 -0
- package/src/lib/record/messages.d.ts +24 -0
- package/src/lib/record/messages.js +25 -0
- package/src/lib/record/messages.js.map +1 -1
- package/src/lib/record/messages.mjs +24 -0
- package/src/lib/record/messages.mjs.map +1 -1
- package/src/lib/record/record-words.d.mts +19 -0
- package/src/lib/record/record-words.d.ts +19 -0
- package/src/lib/record/record-words.js +60 -0
- package/src/lib/record/record-words.js.map +1 -0
- package/src/lib/record/record-words.mjs +56 -0
- package/src/lib/record/record-words.mjs.map +1 -0
- package/src/lib/record/rules.js +1 -1
- package/src/lib/record/rules.js.map +1 -1
- package/src/lib/record/rules.mjs +2 -2
- package/src/lib/record/rules.mjs.map +1 -1
- package/src/lib/record/run.d.mts +39 -1
- package/src/lib/record/run.d.ts +39 -1
- package/src/lib/record/run.js +15 -1
- package/src/lib/record/run.js.map +1 -1
- package/src/lib/record/run.mjs +15 -1
- package/src/lib/record/run.mjs.map +1 -1
- package/src/lib/record/search.js +2 -1
- package/src/lib/record/search.js.map +1 -1
- package/src/lib/record/search.mjs +2 -1
- package/src/lib/record/search.mjs.map +1 -1
- package/src/lib/record/structures.js +3 -3
- package/src/lib/record/structures.js.map +1 -1
- package/src/lib/record/structures.mjs +4 -4
- package/src/lib/record/structures.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/runtime.d.mts +13 -7
- package/src/runtime.d.ts +13 -7
- package/src/runtime.js +13 -3
- package/src/runtime.js.map +1 -1
- package/src/runtime.mjs +4 -1
- package/src/runtime.mjs.map +1 -1
package/src/lib/record/form.mjs
CHANGED
|
@@ -12,8 +12,10 @@ import { firstValues, readLooseMap, readValueMap } from './value-map.mjs';
|
|
|
12
12
|
import { resolveFilter } from './filter.mjs';
|
|
13
13
|
import { rolesAllow } from './roles.mjs';
|
|
14
14
|
import { compileFieldTone, compileTable } from './cells.mjs';
|
|
15
|
-
import {
|
|
15
|
+
import { fillMessage, MESSAGES } from './messages.mjs';
|
|
16
|
+
import { recordSentence } from './record-words.mjs';
|
|
16
17
|
import { loadedRecordOf, saveProblemOf, } from './data-source.mjs';
|
|
18
|
+
import { waitingOn, workedOutFrom } from './waits.mjs';
|
|
17
19
|
import { createUploads, fileBytesIn, uploadGoing, withoutFileBytes } from './uploads.mjs';
|
|
18
20
|
import { hostScheduler } from './scheduler.mjs';
|
|
19
21
|
import { createMoments, createRunner } from './run.mjs';
|
|
@@ -74,7 +76,10 @@ function innerForm(options, within, place, outer) {
|
|
|
74
76
|
// Its time zone written into each date and time field naming none, for whatever reads one from its field alone.
|
|
75
77
|
const page = zonedPage(options.page);
|
|
76
78
|
const scheduler = options.scheduler ?? hostScheduler;
|
|
77
|
-
|
|
79
|
+
// A field's label small inside a message's sentence where the page asks (`lowerLabels`).
|
|
80
|
+
const messages = page.lowerLabels ? { ...(options.messages ?? MESSAGES.en), lowerLabels: true } : (options.messages ?? MESSAGES.en);
|
|
81
|
+
/** One of the form's sentences in the words the page names its records by (`records`), else as the language says it plainly. */
|
|
82
|
+
const named = (noun, plain) => recordSentence(page.records, noun, plain, messages.locale);
|
|
78
83
|
const index = indexLayout(page.layout);
|
|
79
84
|
/** Each field node's answer rules, read once. */
|
|
80
85
|
const nodeRules = new Map();
|
|
@@ -158,6 +163,8 @@ function innerForm(options, within, place, outer) {
|
|
|
158
163
|
const editingNow = () => editing && !(started && state.can?.edit === false);
|
|
159
164
|
const about = () => ({ recordId: started ? state.recordId : (options.recordId ?? null), user: options.user, editing: editingNow(), context: options.context, parent: parentContext });
|
|
160
165
|
const computed = compileComputed(page, today, about);
|
|
166
|
+
/** Each value worked out from others — by the page, or by the app as the lines change — and what it waits for. */
|
|
167
|
+
const workedOut = workedOutFrom(page, !!options.dataSource?.onchange && page.data.kind === 'record', !!options.parent);
|
|
161
168
|
const setWhen = compileSetWhen(page, today, about);
|
|
162
169
|
/** The saved forms placed on the page, by the part's id: each one's answers sit under its name, as JSON. */
|
|
163
170
|
const parts = new Map();
|
|
@@ -317,6 +324,13 @@ function innerForm(options, within, place, outer) {
|
|
|
317
324
|
* 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
|
|
318
325
|
* dialog, a record step's — none is laid on another record, nor over the values the person's Discard let go.
|
|
319
326
|
*/
|
|
327
|
+
/** The data source with a member a record's page asks of it, and the page's model: refused where either is missing. */
|
|
328
|
+
function recordSource(member) {
|
|
329
|
+
const source = options.dataSource;
|
|
330
|
+
if (page.data.kind !== 'record' || !source?.[member])
|
|
331
|
+
throw new Error(`The data source has no ${member}()`);
|
|
332
|
+
return [source, page.data.model];
|
|
333
|
+
}
|
|
320
334
|
function shownNow() {
|
|
321
335
|
const at = generation;
|
|
322
336
|
const reset = resets;
|
|
@@ -848,7 +862,7 @@ function innerForm(options, within, place, outer) {
|
|
|
848
862
|
return undefined;
|
|
849
863
|
const at = (v) => (def.type === 'datetime' ? Date.parse(v) : Date.parse(v.slice(0, 10)));
|
|
850
864
|
if (at(value) < at(start))
|
|
851
|
-
return
|
|
865
|
+
return fillMessage(messages, messages.endBeforeStart, { end: def.label, start: page.fields[node.field].label });
|
|
852
866
|
return undefined;
|
|
853
867
|
}
|
|
854
868
|
/** Errors for the visible fields under `root`, without publishing them. */
|
|
@@ -1237,13 +1251,13 @@ function innerForm(options, within, place, outer) {
|
|
|
1237
1251
|
async function savingNow(chain) {
|
|
1238
1252
|
// A record its source does not let this person change is refused here, in its words or the form's: no source is asked.
|
|
1239
1253
|
if (page.data.kind === 'record' && state.can?.edit === false) {
|
|
1240
|
-
const problem = { kind: 'rule', message: state.can.reason ?? messages.mayNotEdit };
|
|
1254
|
+
const problem = { kind: 'rule', message: state.can.reason ?? named(messages.mayNotEditNoun, messages.mayNotEdit) };
|
|
1241
1255
|
set({ status: 'error', error: problem.message, saveProblem: problem, conflict: null });
|
|
1242
1256
|
return { reason: 'save', message: problem.message };
|
|
1243
1257
|
}
|
|
1244
1258
|
// Asked for the record shown now: another shown while it waits below, it saves nothing of that one, nor says anything there.
|
|
1245
1259
|
const asked = generation;
|
|
1246
|
-
const elsewhere = () => ({ reason: 'save', message: messages.recordReplaced });
|
|
1260
|
+
const elsewhere = () => ({ reason: 'save', message: named(messages.recordReplacedNoun, messages.recordReplaced) });
|
|
1247
1261
|
// What is still typed in a box is read first: it is what is saved, or why nothing is. A name still being looked
|
|
1248
1262
|
// up is waited for, so the record it names is what is saved — or, named by none, why nothing is.
|
|
1249
1263
|
const waiting = readTyped();
|
|
@@ -1443,6 +1457,16 @@ function innerForm(options, within, place, outer) {
|
|
|
1443
1457
|
changed(name, by, { keep });
|
|
1444
1458
|
}
|
|
1445
1459
|
/** A line added: the person's, theirs; a step's, none of theirs, and the lines they changed stay theirs. */
|
|
1460
|
+
/** What a line added now starts with: each line field's default, then what is worked out from its defaultFrom, then the record's lineDefaults. */
|
|
1461
|
+
function newLineValues(field) {
|
|
1462
|
+
const def = lineField(field);
|
|
1463
|
+
const lineFields = def.fields;
|
|
1464
|
+
const start = initialValues(lineFields, today());
|
|
1465
|
+
Object.assign(start, firstValues(lineFields, lineScope(field, start)));
|
|
1466
|
+
if (def.lineDefaults)
|
|
1467
|
+
Object.assign(start, readValueMap(def.lineDefaults, lineFields, recordScope()));
|
|
1468
|
+
return start;
|
|
1469
|
+
}
|
|
1446
1470
|
function addLine(field, values, by) {
|
|
1447
1471
|
const def = lineField(field);
|
|
1448
1472
|
const current = state.values[field] ?? [];
|
|
@@ -1450,13 +1474,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1450
1474
|
let key = `new-${++lineKeys}`;
|
|
1451
1475
|
while (current.some((line) => line.key === key))
|
|
1452
1476
|
key = `new-${++lineKeys}`;
|
|
1453
|
-
|
|
1454
|
-
const lineFields = def.fields;
|
|
1455
|
-
const start = initialValues(lineFields, today());
|
|
1456
|
-
Object.assign(start, firstValues(lineFields, lineScope(field, start)));
|
|
1457
|
-
if (def.lineDefaults)
|
|
1458
|
-
Object.assign(start, readValueMap(def.lineDefaults, lineFields, recordScope()));
|
|
1459
|
-
const line = { key, values: { ...start, ...structuredCopy(values) } };
|
|
1477
|
+
const line = { key, values: { ...newLineValues(field), ...structuredCopy(values) } };
|
|
1460
1478
|
// A new line comes last, so it is numbered after the last.
|
|
1461
1479
|
if (def.sequenceField && line.values[def.sequenceField] == null) {
|
|
1462
1480
|
const numbers = current.map((l) => l.values[def.sequenceField]).filter((n) => typeof n === 'number');
|
|
@@ -1727,7 +1745,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1727
1745
|
return going;
|
|
1728
1746
|
}
|
|
1729
1747
|
async function askToLetGo() {
|
|
1730
|
-
const message = messages.discardChanges;
|
|
1748
|
+
const message = named(messages.discardChangesNoun, messages.discardChanges);
|
|
1731
1749
|
const host = options.host;
|
|
1732
1750
|
let yes = false;
|
|
1733
1751
|
try {
|
|
@@ -1786,7 +1804,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1786
1804
|
const go = letGo({}, chain);
|
|
1787
1805
|
return go === true ? null : go.then((yes) => (yes ? null : kept()));
|
|
1788
1806
|
}
|
|
1789
|
-
const kept = () => ({ reason: 'no', message: messages.discardChanges });
|
|
1807
|
+
const kept = () => ({ reason: 'no', message: named(messages.discardChangesNoun, messages.discardChanges) });
|
|
1790
1808
|
/**
|
|
1791
1809
|
* A step's own move — the record loaded again, or another shown — once the check let what is shown go: kept, the
|
|
1792
1810
|
* person kept their work; replaced, another record was shown before it ended (the run's steps are not for that one).
|
|
@@ -1823,7 +1841,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1823
1841
|
}
|
|
1824
1842
|
/** Why a run stops whose record was replaced while it went on — quietly, when the form was closed. */
|
|
1825
1843
|
function replaced() {
|
|
1826
|
-
return disposed ? { reason: 'closed' } : { reason: 'cannot', message: messages.recordReplaced };
|
|
1844
|
+
return disposed ? { reason: 'closed' } : { reason: 'cannot', message: named(messages.recordReplacedNoun, messages.recordReplaced) };
|
|
1827
1845
|
}
|
|
1828
1846
|
/** The record a record step asks the data source about. */
|
|
1829
1847
|
const record = () => ({ model: page.data.model, id: state.recordId, fields });
|
|
@@ -1933,7 +1951,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1933
1951
|
},
|
|
1934
1952
|
post,
|
|
1935
1953
|
wordsOf: (field) => wordsOfValue(fieldDef(field), state.values[field]),
|
|
1936
|
-
barred: (right) => (state.can?.[right] === false ? state.can.reason ?? messages[MAY_NOT[right]] : null),
|
|
1954
|
+
barred: (right) => (state.can?.[right] === false ? state.can.reason ?? named(messages[`${MAY_NOT[right]}Noun`], messages[MAY_NOT[right]]) : null),
|
|
1937
1955
|
});
|
|
1938
1956
|
// 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
|
|
1939
1957
|
// go when another record is shown by then (its steps were for that one).
|
|
@@ -1961,7 +1979,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1961
1979
|
const builtin = 'builtin' in source && !list.length ? source.builtin : undefined;
|
|
1962
1980
|
if (builtin)
|
|
1963
1981
|
list.push({ do: builtin });
|
|
1964
|
-
const asked = builtin === 'archive' ? messages.archiveConfirm : builtin === 'delete' ? messages.deleteConfirm : undefined;
|
|
1982
|
+
const asked = builtin === 'archive' ? named(messages.archiveConfirmNoun, messages.archiveConfirm) : builtin === 'delete' ? named(messages.deleteConfirmNoun, messages.deleteConfirm) : undefined;
|
|
1965
1983
|
const confirm = 'confirm' in source && source.confirm !== undefined ? source.confirm : asked;
|
|
1966
1984
|
// On the record it was pressed on: another shown while its question is asked, it does nothing to that one.
|
|
1967
1985
|
scope.generation ??= generation;
|
|
@@ -2020,6 +2038,13 @@ function innerForm(options, within, place, outer) {
|
|
|
2020
2038
|
leave: (how) => Promise.resolve(letGo(how)),
|
|
2021
2039
|
leaveNow: (how = {}) => (outer ? outer.leaveNow(how) : atOnce(how)),
|
|
2022
2040
|
recordShown: shownNow,
|
|
2041
|
+
relatedFilter(id) {
|
|
2042
|
+
const node = index.get(id)?.source;
|
|
2043
|
+
if (node?.type !== 'related' || state.recordId == null)
|
|
2044
|
+
return null;
|
|
2045
|
+
const related = node;
|
|
2046
|
+
return [{ field: related.field, op: '=', value: state.recordId }, ...(related.filter ? resolveFilter(related.filter, context()) : [])];
|
|
2047
|
+
},
|
|
2023
2048
|
post,
|
|
2024
2049
|
setEditing(next) {
|
|
2025
2050
|
if (next === editing)
|
|
@@ -2032,6 +2057,10 @@ function innerForm(options, within, place, outer) {
|
|
|
2032
2057
|
loadChoices,
|
|
2033
2058
|
loadDefinitions,
|
|
2034
2059
|
addDefinition,
|
|
2060
|
+
waitsFor(name) {
|
|
2061
|
+
const from = workedOut.get(name);
|
|
2062
|
+
return from ? waitingOn(page, from, state.values, state.unread) : null;
|
|
2063
|
+
},
|
|
2035
2064
|
problem(name) {
|
|
2036
2065
|
fieldDef(name);
|
|
2037
2066
|
// Only a field someone can see is asked anything, and required wherever it is shown so.
|
|
@@ -2110,6 +2139,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2110
2139
|
save: () => track(save()),
|
|
2111
2140
|
canUpload: () => typeof options.dataSource?.upload === 'function',
|
|
2112
2141
|
today,
|
|
2142
|
+
recordReads: (id, values, fields) => recordContext(values, fields ?? page.fields, { recordId: id, user: options.user, editing: false, context: options.context }),
|
|
2113
2143
|
canFindSlots: () => typeof options.dataSource?.slots === 'function',
|
|
2114
2144
|
findSlots(field, ask) {
|
|
2115
2145
|
const source = options.dataSource;
|
|
@@ -2130,6 +2160,28 @@ function innerForm(options, within, place, outer) {
|
|
|
2130
2160
|
values: structuredCopy(state.values),
|
|
2131
2161
|
});
|
|
2132
2162
|
},
|
|
2163
|
+
canFindDuplicates: () => !!(page.data.kind === 'record' && page.duplicates?.fields?.length && options.dataSource?.duplicates),
|
|
2164
|
+
async findDuplicates() {
|
|
2165
|
+
const [source, model] = recordSource('duplicates');
|
|
2166
|
+
const fields = (page.duplicates?.fields ?? []).filter((name) => page.fields[name] && !isEmpty(page.fields[name], state.values[name]));
|
|
2167
|
+
const recordId = state.recordId;
|
|
2168
|
+
// Nothing typed in any of them: nothing asked.
|
|
2169
|
+
if (!fields.length)
|
|
2170
|
+
return [];
|
|
2171
|
+
const { matches } = await source.duplicates({ model, recordId, values: structuredCopy(state.values), fields });
|
|
2172
|
+
// Never the record itself, whatever the source says; the likest first where it scores them.
|
|
2173
|
+
const others = (matches ?? []).filter((match) => match && String(match.id) !== String(recordId));
|
|
2174
|
+
return others.every((match) => typeof match.score === 'number') ? others.sort((a, b) => b.score - a.score) : others;
|
|
2175
|
+
},
|
|
2176
|
+
canMerge: () => !!(page.data.kind === 'record' && options.dataSource?.merge && options.dataSource.load),
|
|
2177
|
+
async readRecord(id) {
|
|
2178
|
+
const [source, model] = recordSource('load');
|
|
2179
|
+
return loadedRecordOf(await source.load({ model, id, fields: page.fields })).values;
|
|
2180
|
+
},
|
|
2181
|
+
async merge(request) {
|
|
2182
|
+
const [source, model] = recordSource('merge');
|
|
2183
|
+
return source.merge({ model, keepId: request.keepId, mergeIds: [...request.mergeIds], values: structuredCopy(request.values), fields: page.fields });
|
|
2184
|
+
},
|
|
2133
2185
|
upload: (field, files, how = {}) => uploads.start(field, files, how),
|
|
2134
2186
|
uploading: (field) => state.uploads.filter((upload) => upload.field === field && !upload.line),
|
|
2135
2187
|
cancelUpload(id) {
|
|
@@ -2242,7 +2294,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2242
2294
|
const sub = def.fields[subfield];
|
|
2243
2295
|
if (!sub)
|
|
2244
2296
|
throw new Error(`The lines of "${field}" have no field "${subfield}"`);
|
|
2245
|
-
const line = (state.values[field] ?? []).find((l) => l.key === key);
|
|
2297
|
+
const line = key === null ? { values: newLineValues(field), id: null } : (state.values[field] ?? []).find((l) => l.key === key);
|
|
2246
2298
|
if (!line)
|
|
2247
2299
|
throw new Error(`"${field}" has no line "${key}"`);
|
|
2248
2300
|
return runSearch(sub, subfield, lineContext(line.values, def.fields, context(), line.id ?? null), query, limit, options);
|
|
@@ -2806,6 +2858,12 @@ function indexLayout(root) {
|
|
|
2806
2858
|
else if (node.type === 'form') {
|
|
2807
2859
|
add(node, parent, 'form', { invisible: node.invisible, readonly: node.readonly });
|
|
2808
2860
|
}
|
|
2861
|
+
else if (node.type === 'related') {
|
|
2862
|
+
add(node, parent, 'other', { invisible: node.invisible });
|
|
2863
|
+
// A row's buttons: their roles here, hidden with the list, their condition read by it on each row's record.
|
|
2864
|
+
for (const button of node.rowButtons ?? [])
|
|
2865
|
+
add(button, node.id, 'button', {});
|
|
2866
|
+
}
|
|
2809
2867
|
else {
|
|
2810
2868
|
add(node, parent, 'other', { invisible: node.invisible });
|
|
2811
2869
|
}
|
|
@@ -2832,6 +2890,9 @@ function indexLayout(root) {
|
|
|
2832
2890
|
// A list holds no fields of its own record: only the buttons for what is selected.
|
|
2833
2891
|
for (const button of root.actions ?? [])
|
|
2834
2892
|
add(button, root.id, 'button', { invisible: button.invisible });
|
|
2893
|
+
// A row's buttons: their roles here, their condition read by the list on each row's record, which the form does not hold.
|
|
2894
|
+
for (const button of root.rowButtons ?? [])
|
|
2895
|
+
add(button, root.id, 'button', {});
|
|
2835
2896
|
}
|
|
2836
2897
|
else {
|
|
2837
2898
|
// The title and statusbar show fields too, so they are validated like
|