@fieldia/core 0.27.0 → 0.29.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.
Files changed (267) hide show
  1. package/README.md +148 -5
  2. package/package.json +1 -1
  3. package/schema/page.schema.json +266 -7
  4. package/src/index.d.mts +10 -2
  5. package/src/index.d.ts +10 -2
  6. package/src/index.js +33 -2
  7. package/src/index.js.map +1 -1
  8. package/src/index.mjs +13 -2
  9. package/src/index.mjs.map +1 -1
  10. package/src/lib/format/approval-checks.d.mts +8 -0
  11. package/src/lib/format/approval-checks.d.ts +8 -0
  12. package/src/lib/format/approval-checks.js +29 -0
  13. package/src/lib/format/approval-checks.js.map +1 -0
  14. package/src/lib/format/approval-checks.mjs +26 -0
  15. package/src/lib/format/approval-checks.mjs.map +1 -0
  16. package/src/lib/format/approval.d.mts +20 -0
  17. package/src/lib/format/approval.d.ts +20 -0
  18. package/src/lib/format/approval.js +25 -0
  19. package/src/lib/format/approval.js.map +1 -0
  20. package/src/lib/format/approval.mjs +22 -0
  21. package/src/lib/format/approval.mjs.map +1 -0
  22. package/src/lib/format/assist-checks.d.mts +14 -0
  23. package/src/lib/format/assist-checks.d.ts +14 -0
  24. package/src/lib/format/assist-checks.js +68 -0
  25. package/src/lib/format/assist-checks.js.map +1 -0
  26. package/src/lib/format/assist-checks.mjs +64 -0
  27. package/src/lib/format/assist-checks.mjs.map +1 -0
  28. package/src/lib/format/assist.d.mts +41 -0
  29. package/src/lib/format/assist.d.ts +41 -0
  30. package/src/lib/format/assist.js +29 -0
  31. package/src/lib/format/assist.js.map +1 -0
  32. package/src/lib/format/assist.mjs +26 -0
  33. package/src/lib/format/assist.mjs.map +1 -0
  34. package/src/lib/format/editable-checks.d.mts +8 -0
  35. package/src/lib/format/editable-checks.d.ts +8 -0
  36. package/src/lib/format/editable-checks.js +38 -0
  37. package/src/lib/format/editable-checks.js.map +1 -0
  38. package/src/lib/format/editable-checks.mjs +35 -0
  39. package/src/lib/format/editable-checks.mjs.map +1 -0
  40. package/src/lib/format/field.d.mts +86 -0
  41. package/src/lib/format/field.d.ts +86 -0
  42. package/src/lib/format/field.js +56 -10
  43. package/src/lib/format/field.js.map +1 -1
  44. package/src/lib/format/field.mjs +55 -9
  45. package/src/lib/format/field.mjs.map +1 -1
  46. package/src/lib/format/languages-checks.d.mts +8 -0
  47. package/src/lib/format/languages-checks.d.ts +8 -0
  48. package/src/lib/format/languages-checks.js +40 -0
  49. package/src/lib/format/languages-checks.js.map +1 -0
  50. package/src/lib/format/languages-checks.mjs +37 -0
  51. package/src/lib/format/languages-checks.mjs.map +1 -0
  52. package/src/lib/format/later-checks.d.mts +4 -3
  53. package/src/lib/format/later-checks.d.ts +4 -3
  54. package/src/lib/format/later-checks.js +4 -3
  55. package/src/lib/format/later-checks.js.map +1 -1
  56. package/src/lib/format/later-checks.mjs +4 -3
  57. package/src/lib/format/later-checks.mjs.map +1 -1
  58. package/src/lib/format/layout.d.mts +123 -28
  59. package/src/lib/format/layout.d.ts +123 -28
  60. package/src/lib/format/layout.js +20 -4
  61. package/src/lib/format/layout.js.map +1 -1
  62. package/src/lib/format/layout.mjs +18 -2
  63. package/src/lib/format/layout.mjs.map +1 -1
  64. package/src/lib/format/layout.types.d.mts +2 -1
  65. package/src/lib/format/layout.types.d.ts +2 -1
  66. package/src/lib/format/list-editable.d.mts +20 -0
  67. package/src/lib/format/list-editable.d.ts +20 -0
  68. package/src/lib/format/list-editable.js +33 -0
  69. package/src/lib/format/list-editable.js.map +1 -0
  70. package/src/lib/format/list-editable.mjs +28 -0
  71. package/src/lib/format/list-editable.mjs.map +1 -0
  72. package/src/lib/format/page.d.mts +91 -9
  73. package/src/lib/format/page.d.ts +91 -9
  74. package/src/lib/format/page.js +4 -0
  75. package/src/lib/format/page.js.map +1 -1
  76. package/src/lib/format/page.mjs +4 -0
  77. package/src/lib/format/page.mjs.map +1 -1
  78. package/src/lib/format/photos-options.d.mts +29 -0
  79. package/src/lib/format/photos-options.d.ts +29 -0
  80. package/src/lib/format/photos-options.js +49 -0
  81. package/src/lib/format/photos-options.js.map +1 -0
  82. package/src/lib/format/photos-options.mjs +45 -0
  83. package/src/lib/format/photos-options.mjs.map +1 -0
  84. package/src/lib/format/references.js +31 -1
  85. package/src/lib/format/references.js.map +1 -1
  86. package/src/lib/format/references.mjs +31 -1
  87. package/src/lib/format/references.mjs.map +1 -1
  88. package/src/lib/format/scan-checks.d.mts +12 -0
  89. package/src/lib/format/scan-checks.d.ts +12 -0
  90. package/src/lib/format/scan-checks.js +89 -0
  91. package/src/lib/format/scan-checks.js.map +1 -0
  92. package/src/lib/format/scan-checks.mjs +86 -0
  93. package/src/lib/format/scan-checks.mjs.map +1 -0
  94. package/src/lib/format/scan-options.d.mts +84 -0
  95. package/src/lib/format/scan-options.d.ts +84 -0
  96. package/src/lib/format/scan-options.js +58 -0
  97. package/src/lib/format/scan-options.js.map +1 -0
  98. package/src/lib/format/scan-options.mjs +54 -0
  99. package/src/lib/format/scan-options.mjs.map +1 -0
  100. package/src/lib/format/upgrade.d.mts +4 -1
  101. package/src/lib/format/upgrade.d.ts +4 -1
  102. package/src/lib/format/upgrade.js +4 -1
  103. package/src/lib/format/upgrade.js.map +1 -1
  104. package/src/lib/format/upgrade.mjs +4 -1
  105. package/src/lib/format/upgrade.mjs.map +1 -1
  106. package/src/lib/format/version.d.mts +2 -2
  107. package/src/lib/format/version.d.ts +2 -2
  108. package/src/lib/format/version.js +2 -2
  109. package/src/lib/format/version.js.map +1 -1
  110. package/src/lib/format/version.mjs +2 -2
  111. package/src/lib/format/version.mjs.map +1 -1
  112. package/src/lib/record/approvals-part.d.mts +6 -0
  113. package/src/lib/record/approvals-part.d.ts +6 -0
  114. package/src/lib/record/approvals-part.js +14 -0
  115. package/src/lib/record/approvals-part.js.map +1 -0
  116. package/src/lib/record/approvals-part.mjs +7 -0
  117. package/src/lib/record/approvals-part.mjs.map +1 -0
  118. package/src/lib/record/approving.d.mts +45 -0
  119. package/src/lib/record/approving.d.ts +45 -0
  120. package/src/lib/record/approving.js +10 -0
  121. package/src/lib/record/approving.js.map +1 -0
  122. package/src/lib/record/approving.mjs +7 -0
  123. package/src/lib/record/approving.mjs.map +1 -0
  124. package/src/lib/record/check.js +7 -1
  125. package/src/lib/record/check.js.map +1 -1
  126. package/src/lib/record/check.mjs +7 -1
  127. package/src/lib/record/check.mjs.map +1 -1
  128. package/src/lib/record/data-source.d.mts +270 -0
  129. package/src/lib/record/data-source.d.ts +270 -0
  130. package/src/lib/record/data-source.js.map +1 -1
  131. package/src/lib/record/data-source.mjs.map +1 -1
  132. package/src/lib/record/day-words.d.mts +10 -0
  133. package/src/lib/record/day-words.d.ts +10 -0
  134. package/src/lib/record/day-words.js +23 -0
  135. package/src/lib/record/day-words.js.map +1 -0
  136. package/src/lib/record/day-words.mjs +20 -0
  137. package/src/lib/record/day-words.mjs.map +1 -0
  138. package/src/lib/record/files-part.d.mts +7 -0
  139. package/src/lib/record/files-part.d.ts +7 -0
  140. package/src/lib/record/files-part.js +167 -0
  141. package/src/lib/record/files-part.js.map +1 -0
  142. package/src/lib/record/files-part.mjs +163 -0
  143. package/src/lib/record/files-part.mjs.map +1 -0
  144. package/src/lib/record/filter.d.mts +5 -0
  145. package/src/lib/record/filter.d.ts +5 -0
  146. package/src/lib/record/filter.js +1 -0
  147. package/src/lib/record/filter.js.map +1 -1
  148. package/src/lib/record/filter.mjs +1 -1
  149. package/src/lib/record/filter.mjs.map +1 -1
  150. package/src/lib/record/form-approvals.d.mts +47 -0
  151. package/src/lib/record/form-approvals.d.ts +47 -0
  152. package/src/lib/record/form-approvals.js +186 -0
  153. package/src/lib/record/form-approvals.js.map +1 -0
  154. package/src/lib/record/form-approvals.mjs +180 -0
  155. package/src/lib/record/form-approvals.mjs.map +1 -0
  156. package/src/lib/record/form-limits.d.mts +5 -8
  157. package/src/lib/record/form-limits.d.ts +5 -8
  158. package/src/lib/record/form-limits.js +20 -21
  159. package/src/lib/record/form-limits.js.map +1 -1
  160. package/src/lib/record/form-limits.mjs +21 -22
  161. package/src/lib/record/form-limits.mjs.map +1 -1
  162. package/src/lib/record/form.d.mts +41 -8
  163. package/src/lib/record/form.d.ts +41 -8
  164. package/src/lib/record/form.js +119 -11
  165. package/src/lib/record/form.js.map +1 -1
  166. package/src/lib/record/form.mjs +119 -11
  167. package/src/lib/record/form.mjs.map +1 -1
  168. package/src/lib/record/languages-part.d.mts +32 -0
  169. package/src/lib/record/languages-part.d.ts +32 -0
  170. package/src/lib/record/languages-part.js +104 -0
  171. package/src/lib/record/languages-part.js.map +1 -0
  172. package/src/lib/record/languages-part.mjs +96 -0
  173. package/src/lib/record/languages-part.mjs.map +1 -0
  174. package/src/lib/record/languages.d.mts +26 -0
  175. package/src/lib/record/languages.d.ts +26 -0
  176. package/src/lib/record/languages.js +20 -0
  177. package/src/lib/record/languages.js.map +1 -0
  178. package/src/lib/record/languages.mjs +15 -0
  179. package/src/lib/record/languages.mjs.map +1 -0
  180. package/src/lib/record/limits.js +4 -2
  181. package/src/lib/record/limits.js.map +1 -1
  182. package/src/lib/record/limits.mjs +4 -2
  183. package/src/lib/record/limits.mjs.map +1 -1
  184. package/src/lib/record/locales/ar.js +6 -0
  185. package/src/lib/record/locales/ar.js.map +1 -1
  186. package/src/lib/record/locales/ar.mjs +6 -0
  187. package/src/lib/record/locales/ar.mjs.map +1 -1
  188. package/src/lib/record/locales/de.js +6 -0
  189. package/src/lib/record/locales/de.js.map +1 -1
  190. package/src/lib/record/locales/de.mjs +6 -0
  191. package/src/lib/record/locales/de.mjs.map +1 -1
  192. package/src/lib/record/locales/fr.js +6 -0
  193. package/src/lib/record/locales/fr.js.map +1 -1
  194. package/src/lib/record/locales/fr.mjs +6 -0
  195. package/src/lib/record/locales/fr.mjs.map +1 -1
  196. package/src/lib/record/memory-approvals.d.mts +47 -0
  197. package/src/lib/record/memory-approvals.d.ts +47 -0
  198. package/src/lib/record/memory-approvals.js +78 -0
  199. package/src/lib/record/memory-approvals.js.map +1 -0
  200. package/src/lib/record/memory-approvals.mjs +75 -0
  201. package/src/lib/record/memory-approvals.mjs.map +1 -0
  202. package/src/lib/record/memory-assist.d.mts +53 -0
  203. package/src/lib/record/memory-assist.d.ts +53 -0
  204. package/src/lib/record/memory-assist.js +519 -0
  205. package/src/lib/record/memory-assist.js.map +1 -0
  206. package/src/lib/record/memory-assist.mjs +513 -0
  207. package/src/lib/record/memory-assist.mjs.map +1 -0
  208. package/src/lib/record/memory-clusters.d.mts +18 -0
  209. package/src/lib/record/memory-clusters.d.ts +18 -0
  210. package/src/lib/record/memory-clusters.js +83 -0
  211. package/src/lib/record/memory-clusters.js.map +1 -0
  212. package/src/lib/record/memory-clusters.mjs +80 -0
  213. package/src/lib/record/memory-clusters.mjs.map +1 -0
  214. package/src/lib/record/memory-data-source.d.mts +43 -3
  215. package/src/lib/record/memory-data-source.d.ts +43 -3
  216. package/src/lib/record/memory-data-source.js +105 -19
  217. package/src/lib/record/memory-data-source.js.map +1 -1
  218. package/src/lib/record/memory-data-source.mjs +105 -19
  219. package/src/lib/record/memory-data-source.mjs.map +1 -1
  220. package/src/lib/record/memory-later.d.mts +1 -1
  221. package/src/lib/record/memory-later.d.ts +1 -1
  222. package/src/lib/record/memory-later.js +4 -1
  223. package/src/lib/record/memory-later.js.map +1 -1
  224. package/src/lib/record/memory-later.mjs +4 -1
  225. package/src/lib/record/memory-later.mjs.map +1 -1
  226. package/src/lib/record/messages.d.mts +24 -2
  227. package/src/lib/record/messages.d.ts +24 -2
  228. package/src/lib/record/messages.js +8 -4
  229. package/src/lib/record/messages.js.map +1 -1
  230. package/src/lib/record/messages.mjs +8 -4
  231. package/src/lib/record/messages.mjs.map +1 -1
  232. package/src/lib/record/option-rules.d.mts +7 -1
  233. package/src/lib/record/option-rules.d.ts +7 -1
  234. package/src/lib/record/option-rules.js +9 -3
  235. package/src/lib/record/option-rules.js.map +1 -1
  236. package/src/lib/record/option-rules.mjs +9 -3
  237. package/src/lib/record/option-rules.mjs.map +1 -1
  238. package/src/lib/record/record-words.d.mts +13 -1
  239. package/src/lib/record/record-words.d.ts +13 -1
  240. package/src/lib/record/record-words.js +29 -1
  241. package/src/lib/record/record-words.js.map +1 -1
  242. package/src/lib/record/record-words.mjs +27 -1
  243. package/src/lib/record/record-words.mjs.map +1 -1
  244. package/src/lib/record/uploads.d.mts +35 -9
  245. package/src/lib/record/uploads.d.ts +35 -9
  246. package/src/lib/record/uploads.js +49 -169
  247. package/src/lib/record/uploads.js.map +1 -1
  248. package/src/lib/record/uploads.mjs +42 -164
  249. package/src/lib/record/uploads.mjs.map +1 -1
  250. package/src/lib/record/values.d.mts +23 -0
  251. package/src/lib/record/values.d.ts +23 -0
  252. package/src/lib/record/values.js +4 -0
  253. package/src/lib/record/values.js.map +1 -1
  254. package/src/lib/record/values.mjs +4 -0
  255. package/src/lib/record/values.mjs.map +1 -1
  256. package/src/lib/record/waits.d.mts +7 -5
  257. package/src/lib/record/waits.d.ts +7 -5
  258. package/src/lib/record/waits.js +7 -10
  259. package/src/lib/record/waits.js.map +1 -1
  260. package/src/lib/record/waits.mjs +7 -10
  261. package/src/lib/record/waits.mjs.map +1 -1
  262. package/src/runtime.d.mts +13 -6
  263. package/src/runtime.d.ts +13 -6
  264. package/src/runtime.js +22 -3
  265. package/src/runtime.js.map +1 -1
  266. package/src/runtime.mjs +11 -2
  267. package/src/runtime.mjs.map +1 -1
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.gridClusters = gridClusters;
4
+ const filter_1 = require("./filter");
5
+ const values_1 = require("./values");
6
+ /**
7
+ * How a memory source answers `clusters` (its `clusters` option), for demos and tests: as a server gathers a map's
8
+ * records by a grid. The records the search finds in the view, each put in a square of the world as the map draws it
9
+ * at its zoom — `radius` pixels a side, the squares fixed to the world, so a record keeps its square as the map pans —
10
+ * and each square holding two or more one bubble: their middle, their count, the box they lie in (and their `weight`,
11
+ * added up, where asked). A square holding one is that record, standing alone, as a list's row gives it (the memory
12
+ * source's `row`: the fields asked, as this person may read them). Those the search finds with no place are counted
13
+ * (`unplaced`). `models` names the models it gathers; a map of any other is answered `false`, and loads its records as
14
+ * without it. A map that moved on (`signal`) is refused with an AbortError. Outside the one-tag script, as
15
+ * `alikeRecords` is.
16
+ *
17
+ * createMemoryDataSource({ records, clusters: gridClusters({ models: ['home'] }) })
18
+ */
19
+ function gridClusters(how = {}) {
20
+ return (request, records, row = (id, values) => ({ id, values: Object.fromEntries(request.fields.map((field) => [field, (0, values_1.structuredCopy)(values[field] ?? null)])) })) => {
21
+ if (request.signal?.aborted)
22
+ throw Object.assign(new Error('The map moved on'), { name: 'AbortError' });
23
+ if (how.models && !how.models.includes(request.model))
24
+ return false;
25
+ const world = 256 * 2 ** request.zoom;
26
+ const side = Math.max(1, request.radius);
27
+ const squares = new Map();
28
+ let unplaced = 0;
29
+ for (const [key, values] of Object.entries(records[request.model] ?? {})) {
30
+ const id = Number.isFinite(Number(key)) ? Number(key) : key;
31
+ if (request.filter.length && !(0, filter_1.matchesFilter)({ id, ...values }, request.filter))
32
+ continue;
33
+ const at = (0, filter_1.placeOf)(values, values[request.field], request.longitude);
34
+ if (!at || Math.abs(at.lat) > 90 || Math.abs(at.lng) > 180) {
35
+ unplaced++;
36
+ continue;
37
+ }
38
+ if (!inBox(at, request.bbox))
39
+ continue;
40
+ // Where it is drawn at this zoom (Web Mercator), in the world's pixels, then its square.
41
+ const sin = Math.min(0.9999, Math.max(-0.9999, Math.sin((at.lat * Math.PI) / 180)));
42
+ const x = ((at.lng + 180) / 360) * world;
43
+ const y = (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * world;
44
+ const name = `${Math.floor(x / side)}:${Math.floor(y / side)}`;
45
+ const square = squares.get(name);
46
+ const weight = request.weight ? Number(values[request.weight]) : 0;
47
+ const weighs = Number.isFinite(weight) ? weight : 0;
48
+ if (!square)
49
+ squares.set(name, { first: { id, values }, count: 1, lat: at.lat, lng: at.lng, box: { south: at.lat, west: at.lng, north: at.lat, east: at.lng }, weight: weighs });
50
+ else {
51
+ square.count++;
52
+ square.lat += at.lat;
53
+ square.lng += at.lng;
54
+ square.weight += weighs;
55
+ square.box = { south: Math.min(square.box.south, at.lat), west: Math.min(square.box.west, at.lng), north: Math.max(square.box.north, at.lat), east: Math.max(square.box.east, at.lng) };
56
+ }
57
+ }
58
+ const clusters = [];
59
+ const points = [];
60
+ for (const square of squares.values()) {
61
+ if (square.count === 1) {
62
+ points.push(row(square.first.id, square.first.values));
63
+ continue;
64
+ }
65
+ const round = (n) => Math.round(n * 1e6) / 1e6;
66
+ clusters.push({
67
+ lat: round(square.lat / square.count),
68
+ lng: round(square.lng / square.count),
69
+ count: square.count,
70
+ bbox: square.box,
71
+ ...(request.weight ? { weight: square.weight } : {}),
72
+ });
73
+ }
74
+ return { clusters, points, unplaced };
75
+ };
76
+ }
77
+ /** Whether a place lies in a box, its edges included: west beyond east crossing the antimeridian. */
78
+ function inBox(at, box) {
79
+ if (at.lat < box.south || at.lat > box.north)
80
+ return false;
81
+ return box.west <= box.east ? at.lng >= box.west && at.lng <= box.east : at.lng >= box.west || at.lng <= box.east;
82
+ }
83
+ //# sourceMappingURL=memory-clusters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-clusters.js","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/record/memory-clusters.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import { matchesFilter, placeOf } from './filter.mjs';
2
+ import { structuredCopy } from './values.mjs';
3
+ /**
4
+ * How a memory source answers `clusters` (its `clusters` option), for demos and tests: as a server gathers a map's
5
+ * records by a grid. The records the search finds in the view, each put in a square of the world as the map draws it
6
+ * at its zoom — `radius` pixels a side, the squares fixed to the world, so a record keeps its square as the map pans —
7
+ * and each square holding two or more one bubble: their middle, their count, the box they lie in (and their `weight`,
8
+ * added up, where asked). A square holding one is that record, standing alone, as a list's row gives it (the memory
9
+ * source's `row`: the fields asked, as this person may read them). Those the search finds with no place are counted
10
+ * (`unplaced`). `models` names the models it gathers; a map of any other is answered `false`, and loads its records as
11
+ * without it. A map that moved on (`signal`) is refused with an AbortError. Outside the one-tag script, as
12
+ * `alikeRecords` is.
13
+ *
14
+ * createMemoryDataSource({ records, clusters: gridClusters({ models: ['home'] }) })
15
+ */
16
+ export function gridClusters(how = {}) {
17
+ return (request, records, row = (id, values) => ({ id, values: Object.fromEntries(request.fields.map((field) => [field, structuredCopy(values[field] ?? null)])) })) => {
18
+ if (request.signal?.aborted)
19
+ throw Object.assign(new Error('The map moved on'), { name: 'AbortError' });
20
+ if (how.models && !how.models.includes(request.model))
21
+ return false;
22
+ const world = 256 * 2 ** request.zoom;
23
+ const side = Math.max(1, request.radius);
24
+ const squares = new Map();
25
+ let unplaced = 0;
26
+ for (const [key, values] of Object.entries(records[request.model] ?? {})) {
27
+ const id = Number.isFinite(Number(key)) ? Number(key) : key;
28
+ if (request.filter.length && !matchesFilter({ id, ...values }, request.filter))
29
+ continue;
30
+ const at = placeOf(values, values[request.field], request.longitude);
31
+ if (!at || Math.abs(at.lat) > 90 || Math.abs(at.lng) > 180) {
32
+ unplaced++;
33
+ continue;
34
+ }
35
+ if (!inBox(at, request.bbox))
36
+ continue;
37
+ // Where it is drawn at this zoom (Web Mercator), in the world's pixels, then its square.
38
+ const sin = Math.min(0.9999, Math.max(-0.9999, Math.sin((at.lat * Math.PI) / 180)));
39
+ const x = ((at.lng + 180) / 360) * world;
40
+ const y = (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * world;
41
+ const name = `${Math.floor(x / side)}:${Math.floor(y / side)}`;
42
+ const square = squares.get(name);
43
+ const weight = request.weight ? Number(values[request.weight]) : 0;
44
+ const weighs = Number.isFinite(weight) ? weight : 0;
45
+ if (!square)
46
+ squares.set(name, { first: { id, values }, count: 1, lat: at.lat, lng: at.lng, box: { south: at.lat, west: at.lng, north: at.lat, east: at.lng }, weight: weighs });
47
+ else {
48
+ square.count++;
49
+ square.lat += at.lat;
50
+ square.lng += at.lng;
51
+ square.weight += weighs;
52
+ square.box = { south: Math.min(square.box.south, at.lat), west: Math.min(square.box.west, at.lng), north: Math.max(square.box.north, at.lat), east: Math.max(square.box.east, at.lng) };
53
+ }
54
+ }
55
+ const clusters = [];
56
+ const points = [];
57
+ for (const square of squares.values()) {
58
+ if (square.count === 1) {
59
+ points.push(row(square.first.id, square.first.values));
60
+ continue;
61
+ }
62
+ const round = (n) => Math.round(n * 1e6) / 1e6;
63
+ clusters.push({
64
+ lat: round(square.lat / square.count),
65
+ lng: round(square.lng / square.count),
66
+ count: square.count,
67
+ bbox: square.box,
68
+ ...(request.weight ? { weight: square.weight } : {}),
69
+ });
70
+ }
71
+ return { clusters, points, unplaced };
72
+ };
73
+ }
74
+ /** Whether a place lies in a box, its edges included: west beyond east crossing the antimeridian. */
75
+ function inBox(at, box) {
76
+ if (at.lat < box.south || at.lat > box.north)
77
+ return false;
78
+ return box.west <= box.east ? at.lng >= box.west && at.lng <= box.east : at.lng >= box.west || at.lng <= box.east;
79
+ }
80
+ //# sourceMappingURL=memory-clusters.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-clusters.mjs","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/record/memory-clusters.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import type { Option, PropertyDefinition } from '../format/field.mjs';
2
- import type { DataSource, OptionsRequest, SubmitRequest, SubmitResult, RecordPermissions, RecordVersion, UploadFile, SlotsRequest, SlotsResult, DuplicatesRequest, DuplicatesResult, LimitsResult, StoredDraft, SaveDraftRequest } from './data-source.mjs';
2
+ import type { DataSource, OptionsRequest, SubmitRequest, SubmitResult, ListedRecord, RecordPermissions, RecordVersion, UploadFile, SlotsRequest, SlotsResult, DuplicatesRequest, DuplicatesResult, LimitsResult, StoredDraft, SaveDraftRequest, ApprovalEntry, SuggestRequest, SuggestResult, RewriteRequest, RewriteResult, SearchToFilterRequest, SearchToFilterResult, ClustersRequest, ClustersResult } from './data-source.mjs';
3
3
  import { type Scheduler } from './scheduler.mjs';
4
4
  import { type MemoryKept } from './memory-kept.mjs';
5
+ import { type MemoryApprovalRoute } from './memory-approvals.mjs';
5
6
  import { type FileValue, type RecordId, type Values } from './values.mjs';
6
7
  export interface MemoryDataSourceOptions {
7
8
  /** Records by model, then by id. */
@@ -114,14 +115,52 @@ export interface MemoryDataSourceOptions {
114
115
  }>;
115
116
  /** The link "Continue later" answers with, made from the draft's key — the app's own address with it: none without it. */
116
117
  draftLink?: (key: string, request: SaveDraftRequest) => string;
118
+ /**
119
+ * How `approvals` answers, by model: the way a record is approved — its levels, who may approve each, any one or all
120
+ * of them, in turn or together, and whether it is sent for approval now — worked out from the record; null where it
121
+ * needs none (an order under its limit). The source keeps each decision as an approval engine would and works out
122
+ * where each level stands; the person deciding is the form's `user`, matched to an approver by id, else by name.
123
+ * Without it, no record needs approval.
124
+ */
125
+ approvals?: Record<string, (values: Values, id: RecordId) => MemoryApprovalRoute | null | undefined>;
126
+ /** Each record's approval history to start from, by `model:id`, oldest first: what was decided before. */
127
+ approvalHistory?: Record<string, ApprovalEntry[]>;
128
+ /**
129
+ * The language a text kept in several (`{ ar, en }`) is read in where this source says it as one — a link's name, a
130
+ * list's order — as an app's server reads it in its person's language: their tag ("ar"), the first typed where it has
131
+ * none in it, or where this is left out. A search finds a link by its name in any of its languages.
132
+ */
133
+ language?: string;
134
+ /**
135
+ * What answers AI help, from the records the source keeps — `assistStandIn()` is a stand-in with no model, reading
136
+ * words by their patterns, for demos and tests. Each member given makes the source's member of that name; without
137
+ * it the source has none, so what asks for it is hidden, as with any source.
138
+ */
139
+ assist?: MemoryAssist;
140
+ /**
141
+ * How `clusters` answers: a list's map's records gathered where they crowd, worked out from the records the source
142
+ * keeps — `gridClusters()` gathers them by a grid, as a server does — or `false` for the map to load them and gather
143
+ * them itself. `row` gives a record standing alone as a list's row is: the fields asked, as this person may read
144
+ * them. Without it, the source has no `clusters`, and every map gathers its records in the browser.
145
+ */
146
+ clusters?: (request: ClustersRequest, records: Readonly<Record<string, Record<string, Values>>>, row: (id: RecordId, values: Values) => ListedRecord) => ClustersResult | false | Promise<ClustersResult | false>;
147
+ }
148
+ /** What a memory source's AI help answers with (`MemoryDataSourceOptions.assist`): each given, from its records. */
149
+ export interface MemoryAssist {
150
+ suggest?(request: SuggestRequest, records: Readonly<Record<string, Record<string, Values>>>): SuggestResult | Promise<SuggestResult>;
151
+ rewrite?(request: RewriteRequest, records: Readonly<Record<string, Record<string, Values>>>): RewriteResult | Promise<RewriteResult>;
152
+ searchToFilter?(request: SearchToFilterRequest, records: Readonly<Record<string, Record<string, Values>>>): SearchToFilterResult | Promise<SearchToFilterResult>;
117
153
  }
154
+ /** The members of AI help a memory source has only where its `assist` answers them. */
155
+ type AssistMembers = 'suggest' | 'rewrite' | 'searchToFilter';
118
156
  /**
119
157
  * Every member of a data source but `exportRecords` and `importRecords`: a
120
158
  * memory source leaves an export and an import to the browser, as a source
121
159
  * without a server of its own does. A test or a demo gives it one by setting
122
- * it.
160
+ * it. AI help's (`suggest`, `rewrite`, `searchToFilter`) where its `assist`
161
+ * option answers them. `clusters` is there where its option says how it answers.
123
162
  */
124
- export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecords' | 'importRecords'>>, Pick<DataSource, 'exportRecords' | 'importRecords'> {
163
+ export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecords' | 'importRecords' | AssistMembers | 'clusters'>>, Pick<DataSource, 'exportRecords' | 'importRecords' | AssistMembers | 'clusters'> {
125
164
  readonly records: Record<string, Record<string, Values>>;
126
165
  /**
127
166
  * The records said to be someone else for one another, by model: each pair of ids, in the order kept, through
@@ -158,3 +197,4 @@ export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecor
158
197
  export declare function createMemoryDataSource(options?: MemoryDataSourceOptions): MemoryDataSource;
159
198
  /** A memory source's members, working on what it keeps (`memoryKept`): the one-tag script's memory part. */
160
199
  export declare function memorySource(options: MemoryDataSourceOptions, kept: MemoryKept): MemoryDataSource;
200
+ export {};
@@ -1,7 +1,8 @@
1
1
  import type { Option, PropertyDefinition } from '../format/field';
2
- import type { DataSource, OptionsRequest, SubmitRequest, SubmitResult, RecordPermissions, RecordVersion, UploadFile, SlotsRequest, SlotsResult, DuplicatesRequest, DuplicatesResult, LimitsResult, StoredDraft, SaveDraftRequest } from './data-source';
2
+ import type { DataSource, OptionsRequest, SubmitRequest, SubmitResult, ListedRecord, RecordPermissions, RecordVersion, UploadFile, SlotsRequest, SlotsResult, DuplicatesRequest, DuplicatesResult, LimitsResult, StoredDraft, SaveDraftRequest, ApprovalEntry, SuggestRequest, SuggestResult, RewriteRequest, RewriteResult, SearchToFilterRequest, SearchToFilterResult, ClustersRequest, ClustersResult } from './data-source';
3
3
  import { type Scheduler } from './scheduler';
4
4
  import { type MemoryKept } from './memory-kept';
5
+ import { type MemoryApprovalRoute } from './memory-approvals';
5
6
  import { type FileValue, type RecordId, type Values } from './values';
6
7
  export interface MemoryDataSourceOptions {
7
8
  /** Records by model, then by id. */
@@ -114,14 +115,52 @@ export interface MemoryDataSourceOptions {
114
115
  }>;
115
116
  /** The link "Continue later" answers with, made from the draft's key — the app's own address with it: none without it. */
116
117
  draftLink?: (key: string, request: SaveDraftRequest) => string;
118
+ /**
119
+ * How `approvals` answers, by model: the way a record is approved — its levels, who may approve each, any one or all
120
+ * of them, in turn or together, and whether it is sent for approval now — worked out from the record; null where it
121
+ * needs none (an order under its limit). The source keeps each decision as an approval engine would and works out
122
+ * where each level stands; the person deciding is the form's `user`, matched to an approver by id, else by name.
123
+ * Without it, no record needs approval.
124
+ */
125
+ approvals?: Record<string, (values: Values, id: RecordId) => MemoryApprovalRoute | null | undefined>;
126
+ /** Each record's approval history to start from, by `model:id`, oldest first: what was decided before. */
127
+ approvalHistory?: Record<string, ApprovalEntry[]>;
128
+ /**
129
+ * The language a text kept in several (`{ ar, en }`) is read in where this source says it as one — a link's name, a
130
+ * list's order — as an app's server reads it in its person's language: their tag ("ar"), the first typed where it has
131
+ * none in it, or where this is left out. A search finds a link by its name in any of its languages.
132
+ */
133
+ language?: string;
134
+ /**
135
+ * What answers AI help, from the records the source keeps — `assistStandIn()` is a stand-in with no model, reading
136
+ * words by their patterns, for demos and tests. Each member given makes the source's member of that name; without
137
+ * it the source has none, so what asks for it is hidden, as with any source.
138
+ */
139
+ assist?: MemoryAssist;
140
+ /**
141
+ * How `clusters` answers: a list's map's records gathered where they crowd, worked out from the records the source
142
+ * keeps — `gridClusters()` gathers them by a grid, as a server does — or `false` for the map to load them and gather
143
+ * them itself. `row` gives a record standing alone as a list's row is: the fields asked, as this person may read
144
+ * them. Without it, the source has no `clusters`, and every map gathers its records in the browser.
145
+ */
146
+ clusters?: (request: ClustersRequest, records: Readonly<Record<string, Record<string, Values>>>, row: (id: RecordId, values: Values) => ListedRecord) => ClustersResult | false | Promise<ClustersResult | false>;
147
+ }
148
+ /** What a memory source's AI help answers with (`MemoryDataSourceOptions.assist`): each given, from its records. */
149
+ export interface MemoryAssist {
150
+ suggest?(request: SuggestRequest, records: Readonly<Record<string, Record<string, Values>>>): SuggestResult | Promise<SuggestResult>;
151
+ rewrite?(request: RewriteRequest, records: Readonly<Record<string, Record<string, Values>>>): RewriteResult | Promise<RewriteResult>;
152
+ searchToFilter?(request: SearchToFilterRequest, records: Readonly<Record<string, Record<string, Values>>>): SearchToFilterResult | Promise<SearchToFilterResult>;
117
153
  }
154
+ /** The members of AI help a memory source has only where its `assist` answers them. */
155
+ type AssistMembers = 'suggest' | 'rewrite' | 'searchToFilter';
118
156
  /**
119
157
  * Every member of a data source but `exportRecords` and `importRecords`: a
120
158
  * memory source leaves an export and an import to the browser, as a source
121
159
  * without a server of its own does. A test or a demo gives it one by setting
122
- * it.
160
+ * it. AI help's (`suggest`, `rewrite`, `searchToFilter`) where its `assist`
161
+ * option answers them. `clusters` is there where its option says how it answers.
123
162
  */
124
- export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecords' | 'importRecords'>>, Pick<DataSource, 'exportRecords' | 'importRecords'> {
163
+ export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecords' | 'importRecords' | AssistMembers | 'clusters'>>, Pick<DataSource, 'exportRecords' | 'importRecords' | AssistMembers | 'clusters'> {
125
164
  readonly records: Record<string, Record<string, Values>>;
126
165
  /**
127
166
  * The records said to be someone else for one another, by model: each pair of ids, in the order kept, through
@@ -158,3 +197,4 @@ export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecor
158
197
  export declare function createMemoryDataSource(options?: MemoryDataSourceOptions): MemoryDataSource;
159
198
  /** A memory source's members, working on what it keeps (`memoryKept`): the one-tag script's memory part. */
160
199
  export declare function memorySource(options: MemoryDataSourceOptions, kept: MemoryKept): MemoryDataSource;
200
+ export {};
@@ -8,6 +8,7 @@ const filter_1 = require("./filter");
8
8
  const periods_1 = require("./periods");
9
9
  const scheduler_1 = require("./scheduler");
10
10
  const memory_kept_1 = require("./memory-kept");
11
+ const memory_approvals_1 = require("./memory-approvals");
11
12
  const values_1 = require("./values");
12
13
  /**
13
14
  * A data source that keeps everything in memory. It implements the whole
@@ -21,6 +22,14 @@ function createMemoryDataSource(options = {}) {
21
22
  function memorySource(options, kept) {
22
23
  const { records, notDuplicatePairs, responses, calls, elsewhere, drafts, table, versionOf, moveOn } = kept;
23
24
  let draftIds = 0;
25
+ /** A record's approvals, as an approval engine keeps them (memory-approvals.ts). */
26
+ const approving = (0, memory_approvals_1.memoryApprovals)({
27
+ routes: options.approvals,
28
+ history: options.approvalHistory,
29
+ table,
30
+ moved: (model, id, by) => (options.versions ? moveOn(model, id, by) : undefined),
31
+ now: () => new Date().toISOString(),
32
+ });
24
33
  /** Properties added in place, by the list and the definitions the app gave for the record they were added to. */
25
34
  const addedDefinitions = new Map();
26
35
  // A new line's id comes after every line id of the records given (kept from an earlier visit, say), never one of them.
@@ -46,10 +55,23 @@ function memorySource(options, kept) {
46
55
  };
47
56
  /** A version and permissions, where this source keeps them. */
48
57
  const told = (version, can) => ({ ...(version ? { version } : {}), ...(can ? { can } : {}) });
58
+ /** A record as a list's row gives it: the fields asked, as this person may read them, with its version and what they may do. */
59
+ const row = (model, id, values, fields) => {
60
+ const can = permissions(model, id);
61
+ return { id, values: readable(Object.fromEntries(fields.map((field) => [field, (0, values_1.structuredCopy)(values[field] ?? null)])), can), ...told(versionOf(model, id), can) };
62
+ };
49
63
  const pause = () => (options.delayMs ? (0, scheduler_1.wait)(options.delayMs, options.scheduler) : Promise.resolve());
64
+ /** A record's name, or its names in each language it is kept in (`{ ar, en }`). */
65
+ const nameOf = (model, id) => table(model)[String(id)]?.[options.labelField?.[model] ?? 'name'];
50
66
  const labelOf = (model, id) => {
51
- const value = table(model)[String(id)]?.[options.labelField?.[model] ?? 'name'];
52
- return typeof value === 'string' ? value : String(id);
67
+ const value = nameOf(model, id);
68
+ const name = oneText(value, options.language);
69
+ return typeof name === 'string' ? name : String(id);
70
+ };
71
+ /** Every name a record goes by: its name, in each of its languages. */
72
+ const namesOf = (model, record) => {
73
+ const value = nameOf(model, record.id);
74
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? Object.values(value).filter((name) => typeof name === 'string') : [record.label];
53
75
  };
54
76
  /** A linked record with what its model shows besides its name. */
55
77
  const shown = (model, record) => {
@@ -85,7 +107,7 @@ function memorySource(options, kept) {
85
107
  /** A record as a filter reads it: its values and its own `id`, which a field of its own named id comes before. */
86
108
  const withId = (key, values) => ({ id: asId(key), ...values });
87
109
  /** The stored records a filter keeps. */
88
- const matchingRows = (model, filter) => Object.entries(table(model)).filter(([key, values]) => (0, filter_1.matchesFilter)(withId(key, values), filter)).map(([, values]) => values);
110
+ const matchingRows = (model, filter) => Object.entries(table(model)).filter(([key, values]) => passes(withId(key, values), filter)).map(([, values]) => values);
89
111
  /** A save, as a backend makes one: refused where the person may not, or over a newer version; else stored, its version moved on. */
90
112
  function write(request) {
91
113
  if (request.id !== null && table(request.model)[String(request.id)]) {
@@ -158,7 +180,7 @@ function memorySource(options, kept) {
158
180
  }
159
181
  return counted;
160
182
  }
161
- return {
183
+ const source = {
162
184
  records,
163
185
  notDuplicatePairs,
164
186
  responses,
@@ -188,6 +210,14 @@ function memorySource(options, kept) {
188
210
  const link = request.later && options.draftLink ? options.draftLink(key, request) : undefined;
189
211
  return { key, ...(link ? { link } : {}) };
190
212
  },
213
+ // A map's records gathered as the option says, each record standing alone as a list's row is.
214
+ ...(options.clusters && {
215
+ async clusters(request) {
216
+ calls.push({ method: 'clusters', request });
217
+ await pause();
218
+ return options.clusters(request, records, (id, values) => row(request.model, id, values, request.fields));
219
+ },
220
+ }),
191
221
  async loadDraft(request) {
192
222
  calls.push({ method: 'loadDraft', request });
193
223
  await pause();
@@ -197,6 +227,16 @@ function memorySource(options, kept) {
197
227
  const { pageId: _page, later: _later, ...draft } = (0, values_1.structuredCopy)(kept);
198
228
  return draft;
199
229
  },
230
+ async approvals(request) {
231
+ calls.push({ method: 'approvals', request });
232
+ await pause();
233
+ return approving.approvals(request);
234
+ },
235
+ async decide(request) {
236
+ calls.push({ method: 'decide', request });
237
+ await pause();
238
+ return approving.decide(request);
239
+ },
200
240
  async load(request) {
201
241
  calls.push({ method: 'load', request });
202
242
  await pause();
@@ -279,12 +319,12 @@ function memorySource(options, kept) {
279
319
  // Found as a person writes it: in any case, with Arabic letters in any of their forms and digits in any script.
280
320
  const query = findable(request.query.trim());
281
321
  return Object.entries(table(request.model))
282
- .filter(([key, values]) => (0, filter_1.matchesFilter)(withId(key, values), request.filter ?? []))
322
+ .filter(([key, values]) => passes(withId(key, values), request.filter ?? []))
283
323
  .map(([key]) => ({ id: asId(key), label: labelOf(request.model, asId(key)) }))
284
324
  // By its name, or a tree's record by its whole path: "Portal\Sprint", "Portal / Sprint"; asked exactly, the name or path itself.
285
325
  .filter((record) => request.exact
286
- ? findable(record.label) === query || findable(pathWords(request.model, record.id)) === query
287
- : findable(record.label).includes(query) || findable(pathWords(request.model, record.id)).includes(query))
326
+ ? namesOf(request.model, record).some((name) => findable(name) === query) || findable(pathWords(request.model, record.id)) === query
327
+ : namesOf(request.model, record).some((name) => findable(name).includes(query)) || findable(pathWords(request.model, record.id)).includes(query))
288
328
  .slice(0, request.limit ?? 8)
289
329
  .map((record) => shown(request.model, record));
290
330
  },
@@ -292,14 +332,14 @@ function memorySource(options, kept) {
292
332
  calls.push({ method: 'list', request });
293
333
  await pause();
294
334
  const matching = Object.entries(table(request.model))
295
- .filter(([key, values]) => (0, filter_1.matchesFilter)(withId(key, values), request.filter))
335
+ .filter(([key, values]) => passes(withId(key, values), request.filter))
296
336
  .map(([key, values]) => ({ id: asId(key), values }));
297
337
  matching.sort((a, b) => {
298
338
  for (const order of request.sort) {
299
339
  // Nearest first: by how far each is, those with no place last.
300
340
  const compared = order.near
301
341
  ? compareValues((0, filter_1.distanceFrom)(a.values, order.field, order.near), (0, filter_1.distanceFrom)(b.values, order.field, order.near))
302
- : compareValues(a.values[order.field], b.values[order.field]);
342
+ : compareValues(oneText(a.values[order.field], options.language), oneText(b.values[order.field], options.language));
303
343
  // Empty last, whichever way the rest goes.
304
344
  if (compared.empty)
305
345
  return compared.result;
@@ -310,18 +350,14 @@ function memorySource(options, kept) {
310
350
  });
311
351
  return {
312
352
  total: matching.length,
313
- records: matching.slice(request.offset, request.offset + request.limit).map(({ id, values }) => {
314
- const can = permissions(request.model, id);
315
- const shownValues = readable(Object.fromEntries(request.fields.map((field) => [field, (0, values_1.structuredCopy)(values[field] ?? null)])), can);
316
- return { id, values: shownValues, ...told(versionOf(request.model, id), can) };
317
- }),
353
+ records: matching.slice(request.offset, request.offset + request.limit).map(({ id, values }) => row(request.model, id, values, request.fields)),
318
354
  ...(request.allIds ? { allIds: matching.map(({ id }) => id) } : {}),
319
355
  };
320
356
  },
321
357
  async groups(request) {
322
358
  calls.push({ method: 'groups', request });
323
359
  await pause();
324
- return groupRows(matchingRows(request.model, request.filter), request.field, request).map(({ rows: _rows, ...group }) => group);
360
+ return groupRows(matchingRows(request.model, request.filter), request.field, request, options.language).map(({ rows: _rows, ...group }) => group);
325
361
  },
326
362
  async slots(request) {
327
363
  calls.push({ method: 'slots', request });
@@ -398,7 +434,7 @@ function memorySource(options, kept) {
398
434
  const rows = matchingRows(request.model, request.filter);
399
435
  const result = { count: rows.length, ...totalsOver(rows, request) };
400
436
  if (request.groupBy)
401
- result.groups = groupRows(rows, request.groupBy, request).map(({ rows: under, ...group }) => ({ ...group, ...totalsOver(under, request) }));
437
+ result.groups = groupRows(rows, request.groupBy, request, options.language).map(({ rows: under, ...group }) => ({ ...group, ...totalsOver(under, request) }));
402
438
  return result;
403
439
  },
404
440
  async upload(file, request) {
@@ -525,6 +561,34 @@ function memorySource(options, kept) {
525
561
  addedDefinitions.set(`${request.list}:${JSON.stringify(own)}`, (0, values_1.structuredCopy)(request.definitions.filter((d) => !own.some((o) => o.name === d.name))));
526
562
  },
527
563
  };
564
+ // AI help's members, where the app gave what answers them (`assist`): each called as a server would be, after the
565
+ // source's pause, from the records it keeps.
566
+ const assist = options.assist;
567
+ if (assist?.suggest) {
568
+ const answer = assist.suggest.bind(assist);
569
+ source.suggest = async (request) => {
570
+ calls.push({ method: 'suggest', request });
571
+ await pause();
572
+ return answer(request, records);
573
+ };
574
+ }
575
+ if (assist?.rewrite) {
576
+ const answer = assist.rewrite.bind(assist);
577
+ source.rewrite = async (request) => {
578
+ calls.push({ method: 'rewrite', request });
579
+ await pause();
580
+ return answer(request, records);
581
+ };
582
+ }
583
+ if (assist?.searchToFilter) {
584
+ const answer = assist.searchToFilter.bind(assist);
585
+ source.searchToFilter = async (request) => {
586
+ calls.push({ method: 'searchToFilter', request });
587
+ await pause();
588
+ return answer(request, records);
589
+ };
590
+ }
591
+ return source;
528
592
  }
529
593
  /** The highest numeric id of a line among the records given — their tables of lines, and lines inside lines — or 0. */
530
594
  function highestLineId(records) {
@@ -558,6 +622,27 @@ function pathOf(value) {
558
622
  const kept = names?.filter((name) => typeof name === 'string' && name.trim() !== '');
559
623
  return kept?.length ? kept : null;
560
624
  }
625
+ /** A text kept in several languages: texts keyed by their languages' tags, { ar, en }. */
626
+ const inLanguages = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length > 0 && Object.entries(value).every(([tag, text]) => typeof text === 'string' && /^[a-z]{2,3}(-[a-z\d]{2,8})*$/i.test(tag));
627
+ /**
628
+ * Whether a record passes a filter (`matchesFilter`), a text it keeps in several languages read in each of them: the
629
+ * record as it reads in one of its languages passes — "villa" finds { ar: "فيلا", en: "Villa" }, "فيلا" too — as a
630
+ * server's search finds a text in any language it keeps.
631
+ */
632
+ function passes(values, filter) {
633
+ const texts = Object.entries(values).filter((entry) => inLanguages(entry[1]));
634
+ if (!texts.length)
635
+ return (0, filter_1.matchesFilter)(values, filter);
636
+ const tags = new Set(texts.flatMap(([, value]) => Object.keys(value)));
637
+ return [...tags].some((tag) => (0, filter_1.matchesFilter)({ ...values, ...Object.fromEntries(texts.map(([name, value]) => [name, value[tag] ?? null])) }, filter));
638
+ }
639
+ /** A text kept in several languages ({ ar, en }) as one, in `language` (the first typed where it has none there); any other value as it is. */
640
+ function oneText(value, language = '') {
641
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || 'id' in value || !Object.values(value).every((text) => typeof text === 'string'))
642
+ return value;
643
+ const texts = value;
644
+ return texts[language]?.trim() ? texts[language] : (Object.values(texts).find((text) => text.trim()) ?? null);
645
+ }
561
646
  /** Two values in order: a link by its label, text by the reader's rules, numbers by size; `empty` says one of them had nothing. */
562
647
  function compareValues(a, b) {
563
648
  const blank = (v) => v === null || v === undefined || v === '';
@@ -570,15 +655,16 @@ function compareValues(a, b) {
570
655
  return { result: String(x).localeCompare(String(y)), empty: false };
571
656
  }
572
657
  /**
573
- * Records under each value of a field: a link's by its record, a record of links (a many2many) under each of its
658
+ * Records under each value of a field (a text kept in several languages by its text in `language`): a link's by its record, a record of links (a many2many) under each of its
574
659
  * links, as a server groups tags; nothing as its own group, last; the rest by their labels. With `interval`, a date's
575
660
  * by the span it falls in, its first day the group's value and label, in time order.
576
661
  */
577
- function groupRows(rows, field, by = {}) {
662
+ function groupRows(rows, field, by = {}, language) {
578
663
  const grouped = new Map();
579
664
  const isLink = (item) => item !== null && typeof item === 'object' && !Array.isArray(item) && 'id' in item;
580
665
  for (const values of rows) {
581
- const held = values[field] ?? null;
666
+ // A text kept in several languages, grouped by its text in the source's language.
667
+ const held = (inLanguages(values[field]) ? oneText(values[field], language) : values[field]) ?? null;
582
668
  // Links: the record under each one; none, under nothing.
583
669
  const each = Array.isArray(held) && !by.interval && held.every(isLink) ? (held.length ? held : [null]) : [held];
584
670
  for (const raw of each) {