@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
|
@@ -4,7 +4,8 @@ import type { Page } from '../format/page.mjs';
|
|
|
4
4
|
import type { JsonValue } from '../format/json.mjs';
|
|
5
5
|
import { type FieldTone, type LineState } from './cells.mjs';
|
|
6
6
|
import { type Messages } from './messages.mjs';
|
|
7
|
-
import { type DataSource, type RecordChanges, type RecordPermissions, type RecordVersion, type SaveProblem, type BotReport, type SlotsResult, type UploadFile } from './data-source.mjs';
|
|
7
|
+
import { type DataSource, type RecordChanges, type RecordPermissions, type RecordVersion, type ResolvedFilter, type SaveProblem, type BotReport, type SlotsResult, type UploadFile, type DuplicateMatch, type MergeResult } from './data-source.mjs';
|
|
8
|
+
import { type WaitsFor } from './waits.mjs';
|
|
8
9
|
import { type FileUpload, type UploadHow } from './uploads.mjs';
|
|
9
10
|
import { type Scheduler } from './scheduler.mjs';
|
|
10
11
|
import { type ActionHost, type ActionRequest, type ActionResult, type OnAction, type PostedMessage, type RunContext, type RunResult } from './run.mjs';
|
|
@@ -327,6 +328,14 @@ export interface Form {
|
|
|
327
328
|
fieldReadonly(name: string): boolean;
|
|
328
329
|
/** What a field would be told if the form were checked now, or null when it passes. Nothing is shown. */
|
|
329
330
|
problem(field: string): string | null;
|
|
331
|
+
/**
|
|
332
|
+
* What a value worked out from others waits for while one of them holds text that does not read (`unread`): a
|
|
333
|
+
* total of the page's own (`compute`) over a table whose line says "abc" in its Quantity, or an amount the app
|
|
334
|
+
* works out as the lines change (read-only, from a source with an onchange) — the line, as people count them, and
|
|
335
|
+
* its cell. Shown, it is a dash said in words, as the table's own total waits: never an amount worked out from the
|
|
336
|
+
* value that text replaced. Null when it shows its value.
|
|
337
|
+
*/
|
|
338
|
+
waitsFor(field: string): WaitsFor | null;
|
|
330
339
|
/**
|
|
331
340
|
* Keep text a person left in a box that does not read (`FormState.unread`), by its path — `field`, or a line's
|
|
332
341
|
* `field.lineKey.subfield` — with why in words; null once it reads, or is let go. Nothing is shown until the box
|
|
@@ -424,6 +433,12 @@ export interface Form {
|
|
|
424
433
|
* (docs/plans/wave1-contract.md, section 10, part 2).
|
|
425
434
|
*/
|
|
426
435
|
recordShown(): () => boolean;
|
|
436
|
+
/**
|
|
437
|
+
* What a related list (a `related` part, by its id) asks the data source's `list` for, on the record shown: the
|
|
438
|
+
* records whose link is this record, and that its own `filter` lets through, each `valueFrom` read from this record.
|
|
439
|
+
* Null before the record's first save — nothing points at it yet — and for no related list of that id.
|
|
440
|
+
*/
|
|
441
|
+
relatedFilter(id: string): ResolvedFilter[] | null;
|
|
427
442
|
/**
|
|
428
443
|
* Post words in the record's conversation, as a `post` step does: through
|
|
429
444
|
* the form's host when it has a way to, else told to the form's own
|
|
@@ -452,8 +467,12 @@ export interface Form {
|
|
|
452
467
|
* exactly the query (`SearchRequest.exact`).
|
|
453
468
|
*/
|
|
454
469
|
search(field: string, query: string, limit?: number, options?: SearchOptions): Promise<RelatedRecord[]>;
|
|
455
|
-
/**
|
|
456
|
-
|
|
470
|
+
/**
|
|
471
|
+
* The same, for a relation inside a one2many line, filtered by that line's values — or structured data of a line
|
|
472
|
+
* naming records of a model (`options.model`). `key` null: a line not added yet, by the values one added now would
|
|
473
|
+
* start with (a row pasted past the last line, its names read before the line is made).
|
|
474
|
+
*/
|
|
475
|
+
searchLine(field: string, key: string | null, subfield: string, query: string, limit?: number, options?: SearchOptions): Promise<RelatedRecord[]>;
|
|
457
476
|
/**
|
|
458
477
|
* The values of records a many2many links to, for a table of them: `fields`
|
|
459
478
|
* of the records with these ids, in the order asked, read with the data
|
|
@@ -532,6 +551,13 @@ export interface Form {
|
|
|
532
551
|
canUpload(): boolean;
|
|
533
552
|
/** Today, as 2026-10-15, on the clock of the page's time zone — the browser's own where it names none — by the form's clock (`now`). */
|
|
534
553
|
today(): string;
|
|
554
|
+
/**
|
|
555
|
+
* A record of the page's model as conditions read it, the form's own way: its values (a link as its id), its `id`,
|
|
556
|
+
* the person using the form (`user`), the app's `context`, and `editing` false — for what reads conditions on records
|
|
557
|
+
* the form does not hold, such as a list's rows (their `rowTones`, `rowBold` and `rowButtons`). Of another model — a
|
|
558
|
+
* related list's rows — with that model's `fields` (the related list's own).
|
|
559
|
+
*/
|
|
560
|
+
recordReads(id: RecordId | null, values: Values, fields?: Record<string, Field | LineField>): Record<string, unknown>;
|
|
535
561
|
/** Whether a date and time drawn as booking slots can ask for its free times: the data source has `slots`. */
|
|
536
562
|
canFindSlots(): boolean;
|
|
537
563
|
/**
|
|
@@ -546,6 +572,28 @@ export interface Form {
|
|
|
546
572
|
length?: number;
|
|
547
573
|
between?: number;
|
|
548
574
|
}): Promise<SlotsResult>;
|
|
575
|
+
/** Whether the page asks for possible duplicates: a record's page naming fields in `duplicates`, its source with `duplicates`. */
|
|
576
|
+
canFindDuplicates(): boolean;
|
|
577
|
+
/**
|
|
578
|
+
* Records like the one shown, asked of the data source's `duplicates` with the fields the page names that hold a
|
|
579
|
+
* value now — none, and nothing is asked — and every value of the form: the matches, the likest first where the
|
|
580
|
+
* source scores them, never the record itself. Rejects where the source has no `duplicates`. Its answer is for the
|
|
581
|
+
* record shown as it was asked (`recordShown`).
|
|
582
|
+
*/
|
|
583
|
+
findDuplicates(): Promise<DuplicateMatch[]>;
|
|
584
|
+
/** Whether records of the page's model can be merged: its source has `merge`, and `load` to read the others. */
|
|
585
|
+
canMerge(): boolean;
|
|
586
|
+
/** Another record of the page's model, as its source loads it, its values alone: the form is left as it is. For a merge's screen. */
|
|
587
|
+
readRecord(id: RecordId): Promise<Values>;
|
|
588
|
+
/**
|
|
589
|
+
* Merge records of the page's model into one through the source's `merge`: `keepId` kept with `values`, `mergeIds`
|
|
590
|
+
* gone. The form is left as it is: show the record kept (`openRecord`, `load`) once it is done.
|
|
591
|
+
*/
|
|
592
|
+
merge(request: {
|
|
593
|
+
keepId: RecordId;
|
|
594
|
+
mergeIds: RecordId[];
|
|
595
|
+
values: Values;
|
|
596
|
+
}): Promise<MergeResult | void>;
|
|
549
597
|
/**
|
|
550
598
|
* Store files picked for a file or picture field — or a field on a table's
|
|
551
599
|
* line (`how.line`) — through the data source's `upload`, each as soon as
|
package/src/lib/record/form.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { Page } from '../format/page';
|
|
|
4
4
|
import type { JsonValue } from '../format/json';
|
|
5
5
|
import { type FieldTone, type LineState } from './cells';
|
|
6
6
|
import { type Messages } from './messages';
|
|
7
|
-
import { type DataSource, type RecordChanges, type RecordPermissions, type RecordVersion, type SaveProblem, type BotReport, type SlotsResult, type UploadFile } from './data-source';
|
|
7
|
+
import { type DataSource, type RecordChanges, type RecordPermissions, type RecordVersion, type ResolvedFilter, type SaveProblem, type BotReport, type SlotsResult, type UploadFile, type DuplicateMatch, type MergeResult } from './data-source';
|
|
8
|
+
import { type WaitsFor } from './waits';
|
|
8
9
|
import { type FileUpload, type UploadHow } from './uploads';
|
|
9
10
|
import { type Scheduler } from './scheduler';
|
|
10
11
|
import { type ActionHost, type ActionRequest, type ActionResult, type OnAction, type PostedMessage, type RunContext, type RunResult } from './run';
|
|
@@ -327,6 +328,14 @@ export interface Form {
|
|
|
327
328
|
fieldReadonly(name: string): boolean;
|
|
328
329
|
/** What a field would be told if the form were checked now, or null when it passes. Nothing is shown. */
|
|
329
330
|
problem(field: string): string | null;
|
|
331
|
+
/**
|
|
332
|
+
* What a value worked out from others waits for while one of them holds text that does not read (`unread`): a
|
|
333
|
+
* total of the page's own (`compute`) over a table whose line says "abc" in its Quantity, or an amount the app
|
|
334
|
+
* works out as the lines change (read-only, from a source with an onchange) — the line, as people count them, and
|
|
335
|
+
* its cell. Shown, it is a dash said in words, as the table's own total waits: never an amount worked out from the
|
|
336
|
+
* value that text replaced. Null when it shows its value.
|
|
337
|
+
*/
|
|
338
|
+
waitsFor(field: string): WaitsFor | null;
|
|
330
339
|
/**
|
|
331
340
|
* Keep text a person left in a box that does not read (`FormState.unread`), by its path — `field`, or a line's
|
|
332
341
|
* `field.lineKey.subfield` — with why in words; null once it reads, or is let go. Nothing is shown until the box
|
|
@@ -424,6 +433,12 @@ export interface Form {
|
|
|
424
433
|
* (docs/plans/wave1-contract.md, section 10, part 2).
|
|
425
434
|
*/
|
|
426
435
|
recordShown(): () => boolean;
|
|
436
|
+
/**
|
|
437
|
+
* What a related list (a `related` part, by its id) asks the data source's `list` for, on the record shown: the
|
|
438
|
+
* records whose link is this record, and that its own `filter` lets through, each `valueFrom` read from this record.
|
|
439
|
+
* Null before the record's first save — nothing points at it yet — and for no related list of that id.
|
|
440
|
+
*/
|
|
441
|
+
relatedFilter(id: string): ResolvedFilter[] | null;
|
|
427
442
|
/**
|
|
428
443
|
* Post words in the record's conversation, as a `post` step does: through
|
|
429
444
|
* the form's host when it has a way to, else told to the form's own
|
|
@@ -452,8 +467,12 @@ export interface Form {
|
|
|
452
467
|
* exactly the query (`SearchRequest.exact`).
|
|
453
468
|
*/
|
|
454
469
|
search(field: string, query: string, limit?: number, options?: SearchOptions): Promise<RelatedRecord[]>;
|
|
455
|
-
/**
|
|
456
|
-
|
|
470
|
+
/**
|
|
471
|
+
* The same, for a relation inside a one2many line, filtered by that line's values — or structured data of a line
|
|
472
|
+
* naming records of a model (`options.model`). `key` null: a line not added yet, by the values one added now would
|
|
473
|
+
* start with (a row pasted past the last line, its names read before the line is made).
|
|
474
|
+
*/
|
|
475
|
+
searchLine(field: string, key: string | null, subfield: string, query: string, limit?: number, options?: SearchOptions): Promise<RelatedRecord[]>;
|
|
457
476
|
/**
|
|
458
477
|
* The values of records a many2many links to, for a table of them: `fields`
|
|
459
478
|
* of the records with these ids, in the order asked, read with the data
|
|
@@ -532,6 +551,13 @@ export interface Form {
|
|
|
532
551
|
canUpload(): boolean;
|
|
533
552
|
/** Today, as 2026-10-15, on the clock of the page's time zone — the browser's own where it names none — by the form's clock (`now`). */
|
|
534
553
|
today(): string;
|
|
554
|
+
/**
|
|
555
|
+
* A record of the page's model as conditions read it, the form's own way: its values (a link as its id), its `id`,
|
|
556
|
+
* the person using the form (`user`), the app's `context`, and `editing` false — for what reads conditions on records
|
|
557
|
+
* the form does not hold, such as a list's rows (their `rowTones`, `rowBold` and `rowButtons`). Of another model — a
|
|
558
|
+
* related list's rows — with that model's `fields` (the related list's own).
|
|
559
|
+
*/
|
|
560
|
+
recordReads(id: RecordId | null, values: Values, fields?: Record<string, Field | LineField>): Record<string, unknown>;
|
|
535
561
|
/** Whether a date and time drawn as booking slots can ask for its free times: the data source has `slots`. */
|
|
536
562
|
canFindSlots(): boolean;
|
|
537
563
|
/**
|
|
@@ -546,6 +572,28 @@ export interface Form {
|
|
|
546
572
|
length?: number;
|
|
547
573
|
between?: number;
|
|
548
574
|
}): Promise<SlotsResult>;
|
|
575
|
+
/** Whether the page asks for possible duplicates: a record's page naming fields in `duplicates`, its source with `duplicates`. */
|
|
576
|
+
canFindDuplicates(): boolean;
|
|
577
|
+
/**
|
|
578
|
+
* Records like the one shown, asked of the data source's `duplicates` with the fields the page names that hold a
|
|
579
|
+
* value now — none, and nothing is asked — and every value of the form: the matches, the likest first where the
|
|
580
|
+
* source scores them, never the record itself. Rejects where the source has no `duplicates`. Its answer is for the
|
|
581
|
+
* record shown as it was asked (`recordShown`).
|
|
582
|
+
*/
|
|
583
|
+
findDuplicates(): Promise<DuplicateMatch[]>;
|
|
584
|
+
/** Whether records of the page's model can be merged: its source has `merge`, and `load` to read the others. */
|
|
585
|
+
canMerge(): boolean;
|
|
586
|
+
/** Another record of the page's model, as its source loads it, its values alone: the form is left as it is. For a merge's screen. */
|
|
587
|
+
readRecord(id: RecordId): Promise<Values>;
|
|
588
|
+
/**
|
|
589
|
+
* Merge records of the page's model into one through the source's `merge`: `keepId` kept with `values`, `mergeIds`
|
|
590
|
+
* gone. The form is left as it is: show the record kept (`openRecord`, `load`) once it is done.
|
|
591
|
+
*/
|
|
592
|
+
merge(request: {
|
|
593
|
+
keepId: RecordId;
|
|
594
|
+
mergeIds: RecordId[];
|
|
595
|
+
values: Values;
|
|
596
|
+
}): Promise<MergeResult | void>;
|
|
549
597
|
/**
|
|
550
598
|
* Store files picked for a file or picture field — or a field on a table's
|
|
551
599
|
* line (`how.line`) — through the data source's `upload`, each as soon as
|
package/src/lib/record/form.js
CHANGED
|
@@ -19,7 +19,9 @@ const filter_1 = require("./filter");
|
|
|
19
19
|
const roles_1 = require("./roles");
|
|
20
20
|
const cells_1 = require("./cells");
|
|
21
21
|
const messages_1 = require("./messages");
|
|
22
|
+
const record_words_1 = require("./record-words");
|
|
22
23
|
const data_source_1 = require("./data-source");
|
|
24
|
+
const waits_1 = require("./waits");
|
|
23
25
|
const uploads_1 = require("./uploads");
|
|
24
26
|
const scheduler_1 = require("./scheduler");
|
|
25
27
|
const run_1 = require("./run");
|
|
@@ -80,7 +82,10 @@ function innerForm(options, within, place, outer) {
|
|
|
80
82
|
// Its time zone written into each date and time field naming none, for whatever reads one from its field alone.
|
|
81
83
|
const page = (0, zoned_1.zonedPage)(options.page);
|
|
82
84
|
const scheduler = options.scheduler ?? scheduler_1.hostScheduler;
|
|
83
|
-
|
|
85
|
+
// A field's label small inside a message's sentence where the page asks (`lowerLabels`).
|
|
86
|
+
const messages = page.lowerLabels ? { ...(options.messages ?? messages_1.MESSAGES.en), lowerLabels: true } : (options.messages ?? messages_1.MESSAGES.en);
|
|
87
|
+
/** One of the form's sentences in the words the page names its records by (`records`), else as the language says it plainly. */
|
|
88
|
+
const named = (noun, plain) => (0, record_words_1.recordSentence)(page.records, noun, plain, messages.locale);
|
|
84
89
|
const index = indexLayout(page.layout);
|
|
85
90
|
/** Each field node's answer rules, read once. */
|
|
86
91
|
const nodeRules = new Map();
|
|
@@ -164,6 +169,8 @@ function innerForm(options, within, place, outer) {
|
|
|
164
169
|
const editingNow = () => editing && !(started && state.can?.edit === false);
|
|
165
170
|
const about = () => ({ recordId: started ? state.recordId : (options.recordId ?? null), user: options.user, editing: editingNow(), context: options.context, parent: parentContext });
|
|
166
171
|
const computed = (0, compute_1.compileComputed)(page, today, about);
|
|
172
|
+
/** Each value worked out from others — by the page, or by the app as the lines change — and what it waits for. */
|
|
173
|
+
const workedOut = (0, waits_1.workedOutFrom)(page, !!options.dataSource?.onchange && page.data.kind === 'record', !!options.parent);
|
|
167
174
|
const setWhen = (0, set_when_1.compileSetWhen)(page, today, about);
|
|
168
175
|
/** The saved forms placed on the page, by the part's id: each one's answers sit under its name, as JSON. */
|
|
169
176
|
const parts = new Map();
|
|
@@ -323,6 +330,13 @@ function innerForm(options, within, place, outer) {
|
|
|
323
330
|
* 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
331
|
* dialog, a record step's — none is laid on another record, nor over the values the person's Discard let go.
|
|
325
332
|
*/
|
|
333
|
+
/** The data source with a member a record's page asks of it, and the page's model: refused where either is missing. */
|
|
334
|
+
function recordSource(member) {
|
|
335
|
+
const source = options.dataSource;
|
|
336
|
+
if (page.data.kind !== 'record' || !source?.[member])
|
|
337
|
+
throw new Error(`The data source has no ${member}()`);
|
|
338
|
+
return [source, page.data.model];
|
|
339
|
+
}
|
|
326
340
|
function shownNow() {
|
|
327
341
|
const at = generation;
|
|
328
342
|
const reset = resets;
|
|
@@ -854,7 +868,7 @@ function innerForm(options, within, place, outer) {
|
|
|
854
868
|
return undefined;
|
|
855
869
|
const at = (v) => (def.type === 'datetime' ? Date.parse(v) : Date.parse(v.slice(0, 10)));
|
|
856
870
|
if (at(value) < at(start))
|
|
857
|
-
return (0, messages_1.
|
|
871
|
+
return (0, messages_1.fillMessage)(messages, messages.endBeforeStart, { end: def.label, start: page.fields[node.field].label });
|
|
858
872
|
return undefined;
|
|
859
873
|
}
|
|
860
874
|
/** Errors for the visible fields under `root`, without publishing them. */
|
|
@@ -1243,13 +1257,13 @@ function innerForm(options, within, place, outer) {
|
|
|
1243
1257
|
async function savingNow(chain) {
|
|
1244
1258
|
// A record its source does not let this person change is refused here, in its words or the form's: no source is asked.
|
|
1245
1259
|
if (page.data.kind === 'record' && state.can?.edit === false) {
|
|
1246
|
-
const problem = { kind: 'rule', message: state.can.reason ?? messages.mayNotEdit };
|
|
1260
|
+
const problem = { kind: 'rule', message: state.can.reason ?? named(messages.mayNotEditNoun, messages.mayNotEdit) };
|
|
1247
1261
|
set({ status: 'error', error: problem.message, saveProblem: problem, conflict: null });
|
|
1248
1262
|
return { reason: 'save', message: problem.message };
|
|
1249
1263
|
}
|
|
1250
1264
|
// Asked for the record shown now: another shown while it waits below, it saves nothing of that one, nor says anything there.
|
|
1251
1265
|
const asked = generation;
|
|
1252
|
-
const elsewhere = () => ({ reason: 'save', message: messages.recordReplaced });
|
|
1266
|
+
const elsewhere = () => ({ reason: 'save', message: named(messages.recordReplacedNoun, messages.recordReplaced) });
|
|
1253
1267
|
// What is still typed in a box is read first: it is what is saved, or why nothing is. A name still being looked
|
|
1254
1268
|
// up is waited for, so the record it names is what is saved — or, named by none, why nothing is.
|
|
1255
1269
|
const waiting = readTyped();
|
|
@@ -1449,6 +1463,16 @@ function innerForm(options, within, place, outer) {
|
|
|
1449
1463
|
changed(name, by, { keep });
|
|
1450
1464
|
}
|
|
1451
1465
|
/** A line added: the person's, theirs; a step's, none of theirs, and the lines they changed stay theirs. */
|
|
1466
|
+
/** 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. */
|
|
1467
|
+
function newLineValues(field) {
|
|
1468
|
+
const def = lineField(field);
|
|
1469
|
+
const lineFields = def.fields;
|
|
1470
|
+
const start = (0, values_1.initialValues)(lineFields, today());
|
|
1471
|
+
Object.assign(start, (0, value_map_1.firstValues)(lineFields, lineScope(field, start)));
|
|
1472
|
+
if (def.lineDefaults)
|
|
1473
|
+
Object.assign(start, (0, value_map_1.readValueMap)(def.lineDefaults, lineFields, recordScope()));
|
|
1474
|
+
return start;
|
|
1475
|
+
}
|
|
1452
1476
|
function addLine(field, values, by) {
|
|
1453
1477
|
const def = lineField(field);
|
|
1454
1478
|
const current = state.values[field] ?? [];
|
|
@@ -1456,13 +1480,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1456
1480
|
let key = `new-${++lineKeys}`;
|
|
1457
1481
|
while (current.some((line) => line.key === key))
|
|
1458
1482
|
key = `new-${++lineKeys}`;
|
|
1459
|
-
|
|
1460
|
-
const lineFields = def.fields;
|
|
1461
|
-
const start = (0, values_1.initialValues)(lineFields, today());
|
|
1462
|
-
Object.assign(start, (0, value_map_1.firstValues)(lineFields, lineScope(field, start)));
|
|
1463
|
-
if (def.lineDefaults)
|
|
1464
|
-
Object.assign(start, (0, value_map_1.readValueMap)(def.lineDefaults, lineFields, recordScope()));
|
|
1465
|
-
const line = { key, values: { ...start, ...(0, values_1.structuredCopy)(values) } };
|
|
1483
|
+
const line = { key, values: { ...newLineValues(field), ...(0, values_1.structuredCopy)(values) } };
|
|
1466
1484
|
// A new line comes last, so it is numbered after the last.
|
|
1467
1485
|
if (def.sequenceField && line.values[def.sequenceField] == null) {
|
|
1468
1486
|
const numbers = current.map((l) => l.values[def.sequenceField]).filter((n) => typeof n === 'number');
|
|
@@ -1733,7 +1751,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1733
1751
|
return going;
|
|
1734
1752
|
}
|
|
1735
1753
|
async function askToLetGo() {
|
|
1736
|
-
const message = messages.discardChanges;
|
|
1754
|
+
const message = named(messages.discardChangesNoun, messages.discardChanges);
|
|
1737
1755
|
const host = options.host;
|
|
1738
1756
|
let yes = false;
|
|
1739
1757
|
try {
|
|
@@ -1792,7 +1810,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1792
1810
|
const go = letGo({}, chain);
|
|
1793
1811
|
return go === true ? null : go.then((yes) => (yes ? null : kept()));
|
|
1794
1812
|
}
|
|
1795
|
-
const kept = () => ({ reason: 'no', message: messages.discardChanges });
|
|
1813
|
+
const kept = () => ({ reason: 'no', message: named(messages.discardChangesNoun, messages.discardChanges) });
|
|
1796
1814
|
/**
|
|
1797
1815
|
* A step's own move — the record loaded again, or another shown — once the check let what is shown go: kept, the
|
|
1798
1816
|
* person kept their work; replaced, another record was shown before it ended (the run's steps are not for that one).
|
|
@@ -1829,7 +1847,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1829
1847
|
}
|
|
1830
1848
|
/** Why a run stops whose record was replaced while it went on — quietly, when the form was closed. */
|
|
1831
1849
|
function replaced() {
|
|
1832
|
-
return disposed ? { reason: 'closed' } : { reason: 'cannot', message: messages.recordReplaced };
|
|
1850
|
+
return disposed ? { reason: 'closed' } : { reason: 'cannot', message: named(messages.recordReplacedNoun, messages.recordReplaced) };
|
|
1833
1851
|
}
|
|
1834
1852
|
/** The record a record step asks the data source about. */
|
|
1835
1853
|
const record = () => ({ model: page.data.model, id: state.recordId, fields });
|
|
@@ -1939,7 +1957,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1939
1957
|
},
|
|
1940
1958
|
post,
|
|
1941
1959
|
wordsOf: (field) => wordsOfValue(fieldDef(field), state.values[field]),
|
|
1942
|
-
barred: (right) => (state.can?.[right] === false ? state.can.reason ?? messages[MAY_NOT[right]] : null),
|
|
1960
|
+
barred: (right) => (state.can?.[right] === false ? state.can.reason ?? named(messages[`${MAY_NOT[right]}Noun`], messages[MAY_NOT[right]]) : null),
|
|
1943
1961
|
});
|
|
1944
1962
|
// 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
1963
|
// go when another record is shown by then (its steps were for that one).
|
|
@@ -1967,7 +1985,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1967
1985
|
const builtin = 'builtin' in source && !list.length ? source.builtin : undefined;
|
|
1968
1986
|
if (builtin)
|
|
1969
1987
|
list.push({ do: builtin });
|
|
1970
|
-
const asked = builtin === 'archive' ? messages.archiveConfirm : builtin === 'delete' ? messages.deleteConfirm : undefined;
|
|
1988
|
+
const asked = builtin === 'archive' ? named(messages.archiveConfirmNoun, messages.archiveConfirm) : builtin === 'delete' ? named(messages.deleteConfirmNoun, messages.deleteConfirm) : undefined;
|
|
1971
1989
|
const confirm = 'confirm' in source && source.confirm !== undefined ? source.confirm : asked;
|
|
1972
1990
|
// On the record it was pressed on: another shown while its question is asked, it does nothing to that one.
|
|
1973
1991
|
scope.generation ??= generation;
|
|
@@ -2026,6 +2044,13 @@ function innerForm(options, within, place, outer) {
|
|
|
2026
2044
|
leave: (how) => Promise.resolve(letGo(how)),
|
|
2027
2045
|
leaveNow: (how = {}) => (outer ? outer.leaveNow(how) : atOnce(how)),
|
|
2028
2046
|
recordShown: shownNow,
|
|
2047
|
+
relatedFilter(id) {
|
|
2048
|
+
const node = index.get(id)?.source;
|
|
2049
|
+
if (node?.type !== 'related' || state.recordId == null)
|
|
2050
|
+
return null;
|
|
2051
|
+
const related = node;
|
|
2052
|
+
return [{ field: related.field, op: '=', value: state.recordId }, ...(related.filter ? (0, filter_1.resolveFilter)(related.filter, context()) : [])];
|
|
2053
|
+
},
|
|
2029
2054
|
post,
|
|
2030
2055
|
setEditing(next) {
|
|
2031
2056
|
if (next === editing)
|
|
@@ -2038,6 +2063,10 @@ function innerForm(options, within, place, outer) {
|
|
|
2038
2063
|
loadChoices,
|
|
2039
2064
|
loadDefinitions,
|
|
2040
2065
|
addDefinition,
|
|
2066
|
+
waitsFor(name) {
|
|
2067
|
+
const from = workedOut.get(name);
|
|
2068
|
+
return from ? (0, waits_1.waitingOn)(page, from, state.values, state.unread) : null;
|
|
2069
|
+
},
|
|
2041
2070
|
problem(name) {
|
|
2042
2071
|
fieldDef(name);
|
|
2043
2072
|
// Only a field someone can see is asked anything, and required wherever it is shown so.
|
|
@@ -2116,6 +2145,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2116
2145
|
save: () => track(save()),
|
|
2117
2146
|
canUpload: () => typeof options.dataSource?.upload === 'function',
|
|
2118
2147
|
today,
|
|
2148
|
+
recordReads: (id, values, fields) => (0, env_1.recordContext)(values, fields ?? page.fields, { recordId: id, user: options.user, editing: false, context: options.context }),
|
|
2119
2149
|
canFindSlots: () => typeof options.dataSource?.slots === 'function',
|
|
2120
2150
|
findSlots(field, ask) {
|
|
2121
2151
|
const source = options.dataSource;
|
|
@@ -2136,6 +2166,28 @@ function innerForm(options, within, place, outer) {
|
|
|
2136
2166
|
values: (0, values_1.structuredCopy)(state.values),
|
|
2137
2167
|
});
|
|
2138
2168
|
},
|
|
2169
|
+
canFindDuplicates: () => !!(page.data.kind === 'record' && page.duplicates?.fields?.length && options.dataSource?.duplicates),
|
|
2170
|
+
async findDuplicates() {
|
|
2171
|
+
const [source, model] = recordSource('duplicates');
|
|
2172
|
+
const fields = (page.duplicates?.fields ?? []).filter((name) => page.fields[name] && !(0, values_1.isEmpty)(page.fields[name], state.values[name]));
|
|
2173
|
+
const recordId = state.recordId;
|
|
2174
|
+
// Nothing typed in any of them: nothing asked.
|
|
2175
|
+
if (!fields.length)
|
|
2176
|
+
return [];
|
|
2177
|
+
const { matches } = await source.duplicates({ model, recordId, values: (0, values_1.structuredCopy)(state.values), fields });
|
|
2178
|
+
// Never the record itself, whatever the source says; the likest first where it scores them.
|
|
2179
|
+
const others = (matches ?? []).filter((match) => match && String(match.id) !== String(recordId));
|
|
2180
|
+
return others.every((match) => typeof match.score === 'number') ? others.sort((a, b) => b.score - a.score) : others;
|
|
2181
|
+
},
|
|
2182
|
+
canMerge: () => !!(page.data.kind === 'record' && options.dataSource?.merge && options.dataSource.load),
|
|
2183
|
+
async readRecord(id) {
|
|
2184
|
+
const [source, model] = recordSource('load');
|
|
2185
|
+
return (0, data_source_1.loadedRecordOf)(await source.load({ model, id, fields: page.fields })).values;
|
|
2186
|
+
},
|
|
2187
|
+
async merge(request) {
|
|
2188
|
+
const [source, model] = recordSource('merge');
|
|
2189
|
+
return source.merge({ model, keepId: request.keepId, mergeIds: [...request.mergeIds], values: (0, values_1.structuredCopy)(request.values), fields: page.fields });
|
|
2190
|
+
},
|
|
2139
2191
|
upload: (field, files, how = {}) => uploads.start(field, files, how),
|
|
2140
2192
|
uploading: (field) => state.uploads.filter((upload) => upload.field === field && !upload.line),
|
|
2141
2193
|
cancelUpload(id) {
|
|
@@ -2248,7 +2300,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2248
2300
|
const sub = def.fields[subfield];
|
|
2249
2301
|
if (!sub)
|
|
2250
2302
|
throw new Error(`The lines of "${field}" have no field "${subfield}"`);
|
|
2251
|
-
const line = (state.values[field] ?? []).find((l) => l.key === key);
|
|
2303
|
+
const line = key === null ? { values: newLineValues(field), id: null } : (state.values[field] ?? []).find((l) => l.key === key);
|
|
2252
2304
|
if (!line)
|
|
2253
2305
|
throw new Error(`"${field}" has no line "${key}"`);
|
|
2254
2306
|
return runSearch(sub, subfield, (0, env_1.lineContext)(line.values, def.fields, context(), line.id ?? null), query, limit, options);
|
|
@@ -2812,6 +2864,12 @@ function indexLayout(root) {
|
|
|
2812
2864
|
else if (node.type === 'form') {
|
|
2813
2865
|
add(node, parent, 'form', { invisible: node.invisible, readonly: node.readonly });
|
|
2814
2866
|
}
|
|
2867
|
+
else if (node.type === 'related') {
|
|
2868
|
+
add(node, parent, 'other', { invisible: node.invisible });
|
|
2869
|
+
// A row's buttons: their roles here, hidden with the list, their condition read by it on each row's record.
|
|
2870
|
+
for (const button of node.rowButtons ?? [])
|
|
2871
|
+
add(button, node.id, 'button', {});
|
|
2872
|
+
}
|
|
2815
2873
|
else {
|
|
2816
2874
|
add(node, parent, 'other', { invisible: node.invisible });
|
|
2817
2875
|
}
|
|
@@ -2838,6 +2896,9 @@ function indexLayout(root) {
|
|
|
2838
2896
|
// A list holds no fields of its own record: only the buttons for what is selected.
|
|
2839
2897
|
for (const button of root.actions ?? [])
|
|
2840
2898
|
add(button, root.id, 'button', { invisible: button.invisible });
|
|
2899
|
+
// A row's buttons: their roles here, their condition read by the list on each row's record, which the form does not hold.
|
|
2900
|
+
for (const button of root.rowButtons ?? [])
|
|
2901
|
+
add(button, root.id, 'button', {});
|
|
2841
2902
|
}
|
|
2842
2903
|
else {
|
|
2843
2904
|
// The title and statusbar show fields too, so they are validated like
|