@fieldia/core 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -3
- package/package.json +1 -1
- package/schema/page.schema.json +1051 -325
- package/src/index.d.mts +9 -2
- package/src/index.d.ts +9 -2
- package/src/index.js +31 -1
- package/src/index.js.map +1 -1
- package/src/index.mjs +12 -2
- package/src/index.mjs.map +1 -1
- package/src/lib/format/actions.d.mts +31 -1
- package/src/lib/format/actions.d.ts +31 -1
- package/src/lib/format/actions.js +1 -0
- package/src/lib/format/actions.js.map +1 -1
- package/src/lib/format/actions.mjs +1 -0
- package/src/lib/format/actions.mjs.map +1 -1
- package/src/lib/format/autofill-group.d.mts +12 -0
- package/src/lib/format/autofill-group.d.ts +12 -0
- package/src/lib/format/autofill-group.js +23 -0
- package/src/lib/format/autofill-group.js.map +1 -0
- package/src/lib/format/autofill-group.mjs +19 -0
- package/src/lib/format/autofill-group.mjs.map +1 -0
- package/src/lib/format/autofill.d.mts +14 -0
- package/src/lib/format/autofill.d.ts +14 -0
- package/src/lib/format/autofill.js +54 -0
- package/src/lib/format/autofill.js.map +1 -0
- package/src/lib/format/autofill.mjs +48 -0
- package/src/lib/format/autofill.mjs.map +1 -0
- package/src/lib/format/bulk-edit.d.mts +18 -0
- package/src/lib/format/bulk-edit.d.ts +18 -0
- package/src/lib/format/bulk-edit.js +26 -0
- package/src/lib/format/bulk-edit.js.map +1 -0
- package/src/lib/format/bulk-edit.mjs +21 -0
- package/src/lib/format/bulk-edit.mjs.map +1 -0
- package/src/lib/format/duplicate-kinds.d.mts +10 -0
- package/src/lib/format/duplicate-kinds.d.ts +10 -0
- package/src/lib/format/duplicate-kinds.js +14 -0
- package/src/lib/format/duplicate-kinds.js.map +1 -0
- package/src/lib/format/duplicate-kinds.mjs +10 -0
- package/src/lib/format/duplicate-kinds.mjs.map +1 -0
- package/src/lib/format/duplicates.d.mts +22 -0
- package/src/lib/format/duplicates.d.ts +22 -0
- package/src/lib/format/duplicates.js +24 -0
- package/src/lib/format/duplicates.js.map +1 -0
- package/src/lib/format/duplicates.mjs +21 -0
- package/src/lib/format/duplicates.mjs.map +1 -0
- package/src/lib/format/field.d.mts +291 -34
- package/src/lib/format/field.d.ts +291 -34
- package/src/lib/format/field.js +19 -4
- package/src/lib/format/field.js.map +1 -1
- package/src/lib/format/field.mjs +17 -2
- package/src/lib/format/field.mjs.map +1 -1
- package/src/lib/format/import-fields.d.mts +18 -0
- package/src/lib/format/import-fields.d.ts +18 -0
- package/src/lib/format/import-fields.js +26 -0
- package/src/lib/format/import-fields.js.map +1 -0
- package/src/lib/format/import-fields.mjs +21 -0
- package/src/lib/format/import-fields.mjs.map +1 -0
- package/src/lib/format/layout.d.mts +7357 -3127
- package/src/lib/format/layout.d.ts +7357 -3127
- package/src/lib/format/layout.js +108 -32
- package/src/lib/format/layout.js.map +1 -1
- package/src/lib/format/layout.mjs +107 -32
- package/src/lib/format/layout.mjs.map +1 -1
- package/src/lib/format/layout.types.d.mts +5 -2
- package/src/lib/format/layout.types.d.ts +5 -2
- package/src/lib/format/page.d.mts +1080 -106
- package/src/lib/format/page.d.ts +1080 -106
- package/src/lib/format/page.js +25 -0
- package/src/lib/format/page.js.map +1 -1
- package/src/lib/format/page.mjs +26 -1
- package/src/lib/format/page.mjs.map +1 -1
- package/src/lib/format/phone.d.mts +27 -0
- package/src/lib/format/phone.d.ts +27 -0
- package/src/lib/format/phone.js +43 -0
- package/src/lib/format/phone.js.map +1 -0
- package/src/lib/format/phone.mjs +39 -0
- package/src/lib/format/phone.mjs.map +1 -0
- package/src/lib/format/references.d.mts +39 -0
- package/src/lib/format/references.d.ts +39 -0
- package/src/lib/format/references.js +263 -20
- package/src/lib/format/references.js.map +1 -1
- package/src/lib/format/references.mjs +263 -20
- package/src/lib/format/references.mjs.map +1 -1
- package/src/lib/format/slots-options.d.mts +26 -0
- package/src/lib/format/slots-options.d.ts +26 -0
- package/src/lib/format/slots-options.js +35 -0
- package/src/lib/format/slots-options.js.map +1 -0
- package/src/lib/format/slots-options.mjs +31 -0
- package/src/lib/format/slots-options.mjs.map +1 -0
- package/src/lib/format/translate.js +8 -4
- package/src/lib/format/translate.js.map +1 -1
- package/src/lib/format/translate.mjs +8 -4
- package/src/lib/format/translate.mjs.map +1 -1
- package/src/lib/format/upgrade.d.mts +8 -3
- package/src/lib/format/upgrade.d.ts +8 -3
- package/src/lib/format/upgrade.js +8 -3
- package/src/lib/format/upgrade.js.map +1 -1
- package/src/lib/format/upgrade.mjs +8 -3
- package/src/lib/format/upgrade.mjs.map +1 -1
- package/src/lib/format/version.d.mts +2 -2
- package/src/lib/format/version.d.ts +2 -2
- package/src/lib/format/version.js +2 -2
- package/src/lib/format/version.js.map +1 -1
- package/src/lib/format/version.mjs +2 -2
- package/src/lib/format/version.mjs.map +1 -1
- package/src/lib/format/zoned.d.mts +8 -0
- package/src/lib/format/zoned.d.ts +8 -0
- package/src/lib/format/zoned.js +36 -0
- package/src/lib/format/zoned.js.map +1 -0
- package/src/lib/format/zoned.mjs +33 -0
- package/src/lib/format/zoned.mjs.map +1 -0
- package/src/lib/record/check.d.mts +6 -1
- package/src/lib/record/check.d.ts +6 -1
- package/src/lib/record/check.js +12 -4
- package/src/lib/record/check.js.map +1 -1
- package/src/lib/record/check.mjs +12 -4
- package/src/lib/record/check.mjs.map +1 -1
- package/src/lib/record/data-source.d.mts +309 -3
- package/src/lib/record/data-source.d.ts +309 -3
- package/src/lib/record/data-source.js +16 -0
- package/src/lib/record/data-source.js.map +1 -1
- package/src/lib/record/data-source.mjs +15 -0
- package/src/lib/record/data-source.mjs.map +1 -1
- package/src/lib/record/form.d.mts +116 -25
- package/src/lib/record/form.d.ts +116 -25
- package/src/lib/record/form.js +457 -78
- package/src/lib/record/form.js.map +1 -1
- package/src/lib/record/form.mjs +457 -78
- package/src/lib/record/form.mjs.map +1 -1
- package/src/lib/record/input-rules.d.mts +51 -9
- package/src/lib/record/input-rules.d.ts +51 -9
- package/src/lib/record/input-rules.js +119 -20
- package/src/lib/record/input-rules.js.map +1 -1
- package/src/lib/record/input-rules.mjs +118 -19
- package/src/lib/record/input-rules.mjs.map +1 -1
- package/src/lib/record/inputs.js +4 -2
- package/src/lib/record/inputs.js.map +1 -1
- package/src/lib/record/inputs.mjs +4 -2
- package/src/lib/record/inputs.mjs.map +1 -1
- package/src/lib/record/limits.d.mts +1 -1
- package/src/lib/record/limits.d.ts +1 -1
- package/src/lib/record/limits.js +4 -2
- package/src/lib/record/limits.js.map +1 -1
- package/src/lib/record/limits.mjs +4 -2
- package/src/lib/record/limits.mjs.map +1 -1
- package/src/lib/record/locales/ar.js +3 -2
- package/src/lib/record/locales/ar.js.map +1 -1
- package/src/lib/record/locales/ar.mjs +3 -2
- package/src/lib/record/locales/ar.mjs.map +1 -1
- package/src/lib/record/locales/de.js +3 -2
- package/src/lib/record/locales/de.js.map +1 -1
- package/src/lib/record/locales/de.mjs +3 -2
- package/src/lib/record/locales/de.mjs.map +1 -1
- package/src/lib/record/locales/fr.js +3 -2
- package/src/lib/record/locales/fr.js.map +1 -1
- package/src/lib/record/locales/fr.mjs +3 -2
- package/src/lib/record/locales/fr.mjs.map +1 -1
- package/src/lib/record/memory-data-source.d.mts +20 -2
- package/src/lib/record/memory-data-source.d.ts +20 -2
- package/src/lib/record/memory-data-source.js +153 -35
- package/src/lib/record/memory-data-source.js.map +1 -1
- package/src/lib/record/memory-data-source.mjs +154 -36
- package/src/lib/record/memory-data-source.mjs.map +1 -1
- package/src/lib/record/memory-duplicates.d.mts +19 -0
- package/src/lib/record/memory-duplicates.d.ts +19 -0
- package/src/lib/record/memory-duplicates.js +78 -0
- package/src/lib/record/memory-duplicates.js.map +1 -0
- package/src/lib/record/memory-duplicates.mjs +74 -0
- package/src/lib/record/memory-duplicates.mjs.map +1 -0
- package/src/lib/record/memory-schedules.d.mts +45 -0
- package/src/lib/record/memory-schedules.d.ts +45 -0
- package/src/lib/record/memory-schedules.js +81 -0
- package/src/lib/record/memory-schedules.js.map +1 -0
- package/src/lib/record/memory-schedules.mjs +78 -0
- package/src/lib/record/memory-schedules.mjs.map +1 -0
- package/src/lib/record/messages.d.mts +3 -1
- package/src/lib/record/messages.d.ts +3 -1
- package/src/lib/record/messages.js +3 -2
- package/src/lib/record/messages.js.map +1 -1
- package/src/lib/record/messages.mjs +3 -2
- package/src/lib/record/messages.mjs.map +1 -1
- package/src/lib/record/periods.d.mts +47 -0
- package/src/lib/record/periods.d.ts +47 -0
- package/src/lib/record/periods.js +86 -0
- package/src/lib/record/periods.js.map +1 -0
- package/src/lib/record/periods.mjs +79 -0
- package/src/lib/record/periods.mjs.map +1 -0
- package/src/lib/record/rules.js +5 -6
- package/src/lib/record/rules.js.map +1 -1
- package/src/lib/record/rules.mjs +5 -6
- package/src/lib/record/rules.mjs.map +1 -1
- package/src/lib/record/run.d.mts +75 -9
- package/src/lib/record/run.d.ts +75 -9
- package/src/lib/record/run.js +169 -26
- package/src/lib/record/run.js.map +1 -1
- package/src/lib/record/run.mjs +170 -27
- package/src/lib/record/run.mjs.map +1 -1
- package/src/lib/record/search.d.mts +13 -3
- package/src/lib/record/search.d.ts +13 -3
- package/src/lib/record/search.js +11 -4
- package/src/lib/record/search.js.map +1 -1
- package/src/lib/record/search.mjs +11 -4
- package/src/lib/record/search.mjs.map +1 -1
- package/src/lib/record/waits.d.mts +32 -0
- package/src/lib/record/waits.d.ts +32 -0
- package/src/lib/record/waits.js +89 -0
- package/src/lib/record/waits.js.map +1 -0
- package/src/lib/record/waits.mjs +85 -0
- package/src/lib/record/waits.mjs.map +1 -0
- package/src/lib/record/zones.d.mts +57 -0
- package/src/lib/record/zones.d.ts +57 -0
- package/src/lib/record/zones.js +147 -0
- package/src/lib/record/zones.js.map +1 -0
- package/src/lib/record/zones.mjs +132 -0
- package/src/lib/record/zones.mjs.map +1 -0
- package/src/runtime.d.mts +19 -9
- package/src/runtime.d.ts +19 -9
- package/src/runtime.js +37 -2
- package/src/runtime.js.map +1 -1
- package/src/runtime.mjs +9 -2
- package/src/runtime.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ list of records — that your app implements;
|
|
|
28
28
|
The page format is also a JSON Schema, for editors and other languages:
|
|
29
29
|
`@fieldia/core/page.schema.json`.
|
|
30
30
|
|
|
31
|
-
A page says which format it is written in (`"fieldia": "0.
|
|
31
|
+
A page says which format it is written in (`"fieldia": "0.4"`, `FORMAT_VERSION`; a page in 0.1 or 0.2 is read as it is).
|
|
32
32
|
One in an older format is brought up to date as it is read — by `checkPage`,
|
|
33
33
|
`validatePage` and `createForm`, so by the viewer and the designer too — and
|
|
34
34
|
`upgradePage` does it for the pages your app keeps. One newer than this Fieldia
|
|
@@ -63,6 +63,58 @@ settings each kind has are `PART_LOOKS`:
|
|
|
63
63
|
}
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## Values that wait for a line
|
|
67
|
+
|
|
68
|
+
A value worked out from others waits while one of them holds text that does
|
|
69
|
+
not read — "abc" in a line's Quantity — as the table's own total does, so it is
|
|
70
|
+
never shown counting the value that text replaced. Who works it out does not
|
|
71
|
+
matter: the page (a `compute`, such as `sum(lines, 'subtotal')`, and what is
|
|
72
|
+
worked out from that in turn) or your app (an amount or totals kept whole, read
|
|
73
|
+
only, which a source with an `onchange` works out as the lines change — a sales
|
|
74
|
+
order's Untaxed, Taxes and Total, Flectra's `tax_totals`). `form.waitsFor(field)`
|
|
75
|
+
says what it waits for — `{ field: 'line_ids', line: 1, column: 'qty' }`, the line
|
|
76
|
+
as people count lines, or `{ field: 'rate' }` for a field of the record's own —
|
|
77
|
+
and null when it shows its value; the viewer draws it as a dash, said in words
|
|
78
|
+
("Fix line 1 of Order lines to see Untaxed amount"). An answer your onchange
|
|
79
|
+
gives meanwhile is kept, and shown once the line reads.
|
|
80
|
+
|
|
81
|
+
## A public form's bot check
|
|
82
|
+
|
|
83
|
+
A page of responses anyone may send — a sign-up, a contact form — can check that
|
|
84
|
+
a person sent it (`botCheck`): a box no person sees or reaches, which a bot
|
|
85
|
+
filling every box fills (`honeypot`, on unless `false`); the fewest seconds a
|
|
86
|
+
person takes from the page shown to its send (`minSeconds`, 3 unless said; 0
|
|
87
|
+
tells none); and the slot the app draws its own captcha in (`captcha`: a slot's
|
|
88
|
+
name — Turnstile, reCAPTCHA, hCaptcha):
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
"layout": { "type": "sections", "id": "signup", "children": [
|
|
92
|
+
…,
|
|
93
|
+
{ "type": "slot", "id": "s-captcha", "name": "captcha" }
|
|
94
|
+
] },
|
|
95
|
+
"botCheck": { "minSeconds": 3, "captcha": "captcha" }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Nothing is refused for it on the page: each send tells the app what it saw,
|
|
99
|
+
beside the answers (`submit({ pageId, values, bot })`, and the `send` event), and
|
|
100
|
+
the app decides — keep the response aside, check the token with its service, or
|
|
101
|
+
refuse it:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
async submit({ pageId, values, bot }) {
|
|
105
|
+
// bot: { honeypot: false, tooFast: false, seconds: 41.2, captcha: '0.Zx…' }
|
|
106
|
+
if (bot?.honeypot || bot?.tooFast) return keepAside(pageId, values);
|
|
107
|
+
if (bot && !(await verifyTurnstile(bot.captcha))) throw saveRefused({ kind: 'rule', message: 'Tick “I am human” first' });
|
|
108
|
+
return store(pageId, values);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The app's captcha widget hands its token to the form, `form.botCheck({ captcha: token })`
|
|
113
|
+
(null once it expires); a token goes once it is sent, as a captcha's is spent. The
|
|
114
|
+
viewer draws the hidden box and tells the form when it is filled
|
|
115
|
+
(`form.botCheck({ honeypot: true })`). A page without `botCheck` sends what it
|
|
116
|
+
always did.
|
|
117
|
+
|
|
66
118
|
## Files stored as they are picked
|
|
67
119
|
|
|
68
120
|
Give the data source `upload(file, request)` and a file or picture field sends
|
|
@@ -96,6 +148,112 @@ your words and offers no Retry; any other error offers Retry. Stop with an
|
|
|
96
148
|
one. Without `upload`, files travel inside the record as base64, as before.
|
|
97
149
|
See [files stored as they are picked](https://fieldia.dev/data/#upload).
|
|
98
150
|
|
|
151
|
+
## Lists that add up, and filter by time
|
|
152
|
+
|
|
153
|
+
A list asks its source's optional `totals({ model, filter, totals, groupBy?,
|
|
154
|
+
interval?, currencies? })` for a column's sum, average, lowest, highest or
|
|
155
|
+
count — over the search, under each group, or the rows ticked (`id in` their
|
|
156
|
+
ids) — and its `groups` for a date by the span it falls in (`interval`: `day`,
|
|
157
|
+
`week`, `month`, `quarter` or `year`, with `weekStart` and `timeZone`), each
|
|
158
|
+
group's value its span's first day. An amount named in `currencies` (by the
|
|
159
|
+
field holding each record's currency) is totalled within one currency only:
|
|
160
|
+
over several, its sum is `null`, and `currencies` in the answer names the one
|
|
161
|
+
it is in. A period a list's filter names ("This month", "Overdue") arrives as
|
|
162
|
+
plain conditions. `spanStart(value, interval, { weekStart, timeZone })` finds
|
|
163
|
+
the span a value falls in as the list counts it, and `dayIn` and `dayStarts`
|
|
164
|
+
a day in a time zone; `createMemoryDataSource` does it all. See
|
|
165
|
+
[lists that add up](https://fieldia.dev/lists/#totals).
|
|
166
|
+
|
|
167
|
+
## Time zones and booking slots
|
|
168
|
+
|
|
169
|
+
A page may name the time zone its dates and times are in, by its IANA name —
|
|
170
|
+
a branch's, a clinic's — and a date and time field may name its own:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{ "timeZone": "Europe/Berlin", "fields": { "flight": { "type": "datetime", "label": "Departs", "timeZone": "Africa/Cairo" } } }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A date and time is still kept as an instant, as before (`2026-10-15T12:30:00.000Z`;
|
|
177
|
+
one written with its offset is read the same). It is shown and typed on the
|
|
178
|
+
zone's clock wherever the person is, with the zone's short name beside it where
|
|
179
|
+
the browser's zone is another ("14:30 CEST"). Today is the zone's day: a date's
|
|
180
|
+
default `"today"`, its earliest and latest counted from today, `today()`, a
|
|
181
|
+
rule's past and future, and `form.today()`. A time the clocks skip, or repeat, on
|
|
182
|
+
a change day is said, never moved. `zonedPage(page)` writes the page's zone into
|
|
183
|
+
each date and time field that names none, as the form and the viewer read it;
|
|
184
|
+
`wallTime`, `instantsAt`, `dayIn` and `clocksSkip` are the clock arithmetic
|
|
185
|
+
they use. `checkPage` and `validatePage` refuse a zone the browser does not know.
|
|
186
|
+
Without `timeZone`, the browser's own zone, as before.
|
|
187
|
+
|
|
188
|
+
A date and time drawn as booking slots (`"widget": "slots"`) asks the data
|
|
189
|
+
source's `slots(request)` for its free times: the days asked (`from`, `to`, in
|
|
190
|
+
`timeZone`), the schedule its node names (`options.schedule`), the booking's
|
|
191
|
+
`length` and the minutes `between` two as the page says, the record and its
|
|
192
|
+
values. It answers each day's state — `open`, `full`, `closed`, `holiday` with a
|
|
193
|
+
`note` — and the times free, as instants, and may say the booking's `length`.
|
|
194
|
+
`form.findSlots(field, { from, to })` asks it for a widget of your own;
|
|
195
|
+
`form.canFindSlots()` says whether the source can. Without `slots`, the field is
|
|
196
|
+
the plain date and time box. For demos and tests, `scheduleSlots(schedules)`
|
|
197
|
+
answers from booking schedules — hours by the day of the week on a zone's clock,
|
|
198
|
+
a booking's length, the minutes kept free after one, holidays, and the records
|
|
199
|
+
the bookings are:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { createMemoryDataSource, scheduleSlots } from '@fieldia/core';
|
|
203
|
+
|
|
204
|
+
const weekday: [string, string][] = [['09:00', '13:00'], ['14:00', '18:00']];
|
|
205
|
+
const dataSource = createMemoryDataSource({
|
|
206
|
+
records,
|
|
207
|
+
slots: scheduleSlots({
|
|
208
|
+
showroom: {
|
|
209
|
+
timeZone: 'Europe/Berlin',
|
|
210
|
+
hours: { 1: weekday, 2: weekday, 3: weekday, 4: weekday, 5: weekday, 6: [['10:00', '14:00']] },
|
|
211
|
+
length: 45,
|
|
212
|
+
between: 15,
|
|
213
|
+
holidays: { '2026-12-25': 'Christmas Day' },
|
|
214
|
+
bookings: { model: 'drive.booking', field: 'start', endField: 'stop', same: ['car'] },
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Possible duplicates and Merge
|
|
221
|
+
|
|
222
|
+
A record's page may name the fields records like the one typed are found by —
|
|
223
|
+
the same phone, email or tax id, a name close enough:
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{ "duplicates": { "fields": ["name", "phone", "email", "vat"], "on": "type" } }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The data source's `duplicates({ model, recordId, values, fields })` is asked
|
|
230
|
+
with the fields named that hold a value (none: nothing is asked), every value of
|
|
231
|
+
the form and the record typed (null while new), and answers with the records
|
|
232
|
+
like it — `{ matches: [{ id, name, why: ['phone'], score? }] }`, `why` naming
|
|
233
|
+
the fields each matched on. `on: "type"` (the default) asks as those fields are
|
|
234
|
+
typed in, as a saved record opens and as it is saved; `"save"` only as it is
|
|
235
|
+
saved. Nothing is in the way of a save. `merge({ model, keepId, mergeIds,
|
|
236
|
+
values, fields })` keeps `keepId` with the value picked for each field and takes
|
|
237
|
+
`mergeIds` away; moving what was theirs (messages, activities, links) is the
|
|
238
|
+
source's work, and `saveRefused({ kind: 'rule', message })` refuses it.
|
|
239
|
+
`form.findDuplicates()`, `form.readRecord(id)` and `form.merge(…)` ask the
|
|
240
|
+
source for a screen of your own, `form.canFindDuplicates()` and
|
|
241
|
+
`form.canMerge()` say whether it can; the viewer says each match under the field
|
|
242
|
+
it matched on, with Open it, "It's someone new" and Merge (`@fieldia/viewer`).
|
|
243
|
+
`checkPage` and `validatePage` refuse a field the page has not, one named twice,
|
|
244
|
+
and one of a kind records are not compared by (`DUPLICATE_KINDS`). Without
|
|
245
|
+
`duplicates` in the source, nothing is asked. For demos and tests,
|
|
246
|
+
`alikeRecords()` answers from the memory source's records — the same value as
|
|
247
|
+
people write them apart (a phone's spaces, an email's case, a link's record) or,
|
|
248
|
+
in the name field, a name close enough (`nameLikeness`) — and its `merge`
|
|
249
|
+
merges as a backend would:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { alikeRecords, createMemoryDataSource } from '@fieldia/core';
|
|
253
|
+
|
|
254
|
+
const dataSource = createMemoryDataSource({ records, duplicates: alikeRecords() });
|
|
255
|
+
```
|
|
256
|
+
|
|
99
257
|
## Two people, one record
|
|
100
258
|
|
|
101
259
|
A source that keeps versions returns each record with its version, and what this
|
|
@@ -141,7 +299,8 @@ values. It asks only about the person's work at risk (`workAtRisk(state)`):
|
|
|
141
299
|
each change of theirs the last finished save of the record did not send —
|
|
142
300
|
typed while it was on its way, while the run that saved went on, or since
|
|
143
301
|
(`state.unsent`, told against the record as that save left it; a value set by
|
|
144
|
-
a step, a rule, a compute or your app is never theirs
|
|
302
|
+
a step, a rule, a compute or your app is never theirs; text they left since
|
|
303
|
+
that does not read, "12a", counts as "12" does) — a file the store holds
|
|
145
304
|
that no save sent, files still on their way, and changes Reload set aside. Then
|
|
146
305
|
it asks "Discard your changes?" through the host's `ask(message, { discard:
|
|
147
306
|
true })`, or `confirm`; Cancel keeps the person where they are, and with
|
|
@@ -150,13 +309,62 @@ out behaves as it did in 0.20: a change not saved goes where a step leaves the
|
|
|
150
309
|
record. `form.leaveNow()` says whether a way out can go at once. Call
|
|
151
310
|
`form.leave()` before your app's own ways out — a route change, an unmount —
|
|
152
311
|
and `unsavedWork(state)` (any change, file or change set aside) for a browser's
|
|
153
|
-
`beforeunload
|
|
312
|
+
`beforeunload`, or let the viewer keep it (`warnOnLeave`). A save refused for a
|
|
313
|
+
conflict, waiting for Reload or Keep mine, is at risk too: the person asked for
|
|
314
|
+
it to be saved. Discard lets the changes no save took go back to the record as
|
|
315
|
+
the last save left it. Every answer that comes late — an onchange, a step's
|
|
316
|
+
Archive, Duplicate or Delete, Keep mine's read, a name a box looks up — is laid
|
|
317
|
+
only on the record it was asked for: anything of your own that waits and then
|
|
318
|
+
writes on the form asks `form.recordShown()` first (taken as it asks, true
|
|
319
|
+
while that record is still shown, as it was). What `can` does
|
|
154
320
|
not allow is kept to before any source is asked: fields locked or hidden (and
|
|
155
321
|
left out of what a save sends), a save refused with the source's `reason`, the
|
|
156
322
|
gear menu's Archive, Duplicate and Delete and the buttons whose steps would do
|
|
157
323
|
them hidden. `createMemoryDataSource({ versions: { by: 'Sara' }, can })` does
|
|
158
324
|
it all in memory, and `changeElsewhere` changes a record as someone else would.
|
|
159
325
|
|
|
326
|
+
## Many records at once
|
|
327
|
+
|
|
328
|
+
A list's "Select all 1,284 that match", its Export and its Edit of many
|
|
329
|
+
records read three optional members. `list({ …, allIds: true })` answers with
|
|
330
|
+
`allIds`, every matching id in the list's order (fewer than `total` means the
|
|
331
|
+
source stopped short, and the list says so). `writeMany({ model, ids, values,
|
|
332
|
+
fields, versions })` writes one value in a field of each record and answers
|
|
333
|
+
`{ failed: [{ id, message, problem? }] }`; without it the list saves each record
|
|
334
|
+
in turn (`save` with `partial: true` and the version it was listed at).
|
|
335
|
+
`exportRecords(request)` writes an export on the server: answer with the file
|
|
336
|
+
(`{ name, data }`, data a Blob, bytes or text, or `{ name, url }`), with
|
|
337
|
+
`{ message }` when it is handed over another way, or with `false` to let the
|
|
338
|
+
browser write it. A page offers Edit with its list's `bulkEdit` (`true`, or the
|
|
339
|
+
fields), and Export with `"export": true` — each off unless asked; `bulkEditFields(page, list)`
|
|
340
|
+
says which fields Edit offers. `createMemoryDataSource` gives `allIds` and
|
|
341
|
+
`writeMany` (each record refused as its save would be), and no `exportRecords`.
|
|
342
|
+
See [many records at once](https://fieldia.dev/data/#many).
|
|
343
|
+
|
|
344
|
+
## Records that point at this one
|
|
345
|
+
|
|
346
|
+
A page's `related` part lists another model's records whose link is this
|
|
347
|
+
record — a customer's invoices — inside it (the viewer draws it). Its
|
|
348
|
+
`model`, `field` (their many2one to this page's model), `fields` and `columns`
|
|
349
|
+
say what it lists; `filter`, `sort`, `limit`, `cells`, `searchFields`,
|
|
350
|
+
`viewAll` and `create` the rest. `form.relatedFilter(id)` gives what it asks
|
|
351
|
+
the data source's `list` for, as the source gets it: the link to the record
|
|
352
|
+
shown, then its own filter with each `valueFrom` read from that record — or null
|
|
353
|
+
before the record's first save. No new data source member: `list` answers it.
|
|
354
|
+
The page check holds its columns, order, search and cells to its own `fields`,
|
|
355
|
+
its link to a many2one of theirs to this model, and it to a record's page.
|
|
356
|
+
|
|
357
|
+
A list's Import makes records from a file's rows. `createMany({ model, records,
|
|
358
|
+
fields })` makes many at once and answers each in order: `{ id }`, or `{ message,
|
|
359
|
+
problem? }` for one it refused; without it the list saves each as a new record,
|
|
360
|
+
`save(newRecordSave(model, fields, values))`. `importRecords({ model, file,
|
|
361
|
+
columns, rows, fields, locale, dateOrder })` takes the file on the server once
|
|
362
|
+
its columns are matched: answer `{ message }`, or `false` to let the browser
|
|
363
|
+
import it. A page offers Import with its list's `import` (`true`, or the
|
|
364
|
+
fields); `importFields(page, list)` says which fields a file's columns may fill.
|
|
365
|
+
`createMemoryDataSource` gives `createMany`. See [many records made at
|
|
366
|
+
once](https://fieldia.dev/data/#import).
|
|
367
|
+
|
|
160
368
|
## A saved form placed in another
|
|
161
369
|
|
|
162
370
|
A block made once — an address, a contact person, a consent — is a page of its
|
package/package.json
CHANGED