@chelone/core 0.6.0 → 0.9.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 (255) hide show
  1. package/dist/assistant/assistant-session.d.ts +6 -0
  2. package/dist/assistant/assistant-session.js +204 -181
  3. package/dist/assistant/echo-transport.js +26 -29
  4. package/dist/assistant/http-transport.d.ts +10 -0
  5. package/dist/assistant/http-transport.js +98 -0
  6. package/dist/assistant/types.d.ts +1 -7
  7. package/dist/auth/access-control.d.ts +13 -5
  8. package/dist/auth/access-control.js +25 -21
  9. package/dist/auth/auth-options.d.ts +23 -17
  10. package/dist/auth/auth-options.js +82 -48
  11. package/dist/auth/permission-matrix.d.ts +12 -0
  12. package/dist/auth/permission-matrix.js +67 -0
  13. package/dist/auth/types.d.ts +14 -4
  14. package/dist/block-editor/block-document.d.ts +64 -0
  15. package/dist/block-editor/block-document.js +147 -0
  16. package/dist/block-editor/data-blocks.d.ts +63 -0
  17. package/dist/block-editor/data-blocks.js +117 -0
  18. package/dist/chart/chart-data.js +46 -44
  19. package/dist/command-menu/hotkey.js +58 -47
  20. package/dist/command-menu/recents.d.ts +4 -0
  21. package/dist/command-menu/recents.js +12 -11
  22. package/dist/command-menu/registry.d.ts +9 -5
  23. package/dist/command-menu/registry.js +36 -46
  24. package/dist/controller/calendar-dates.d.ts +12 -0
  25. package/dist/controller/calendar-dates.js +94 -113
  26. package/dist/controller/calendar-events.js +93 -88
  27. package/dist/controller/client-list-controller.d.ts +2 -2
  28. package/dist/controller/client-list-controller.js +251 -254
  29. package/dist/controller/data-table-columns.d.ts +2 -0
  30. package/dist/controller/data-table-columns.js +50 -56
  31. package/dist/controller/deep-equal.d.ts +1 -0
  32. package/dist/controller/deep-equal.js +14 -0
  33. package/dist/controller/filter-toggle.js +36 -44
  34. package/dist/controller/in-place-editor.d.ts +43 -0
  35. package/dist/controller/in-place-editor.js +43 -0
  36. package/dist/controller/infinite-list-controller.js +219 -189
  37. package/dist/controller/inspector.d.ts +55 -0
  38. package/dist/controller/inspector.js +70 -0
  39. package/dist/controller/job-monitor.d.ts +10 -4
  40. package/dist/controller/job-monitor.js +46 -47
  41. package/dist/controller/kanban-board.js +45 -24
  42. package/dist/controller/list-controller.d.ts +8 -5
  43. package/dist/controller/list-controller.js +447 -400
  44. package/dist/controller/list-params.js +96 -87
  45. package/dist/controller/pivot-grid.d.ts +4 -4
  46. package/dist/controller/pivot-grid.js +89 -95
  47. package/dist/controller/prev-next.d.ts +2 -2
  48. package/dist/controller/prev-next.js +74 -54
  49. package/dist/controller/record-controllers.d.ts +14 -6
  50. package/dist/controller/record-controllers.js +288 -257
  51. package/dist/controller/record-diff.d.ts +21 -0
  52. package/dist/controller/record-diff.js +50 -0
  53. package/dist/controller/record-timeline.d.ts +5 -5
  54. package/dist/controller/record-timeline.js +42 -30
  55. package/dist/controller/record-tree.js +57 -47
  56. package/dist/controller/record-workflow.d.ts +38 -16
  57. package/dist/controller/record-workflow.js +85 -64
  58. package/dist/controller/reference-many-controller.d.ts +2 -0
  59. package/dist/controller/reference-many-controller.js +199 -192
  60. package/dist/controller/reference-one-controller.d.ts +1 -1
  61. package/dist/controller/reference-one-controller.js +63 -56
  62. package/dist/controller/saved-queries.js +104 -91
  63. package/dist/controller/translate-fallback.js +10 -6
  64. package/dist/csv/export.d.ts +4 -3
  65. package/dist/csv/export.js +43 -40
  66. package/dist/csv/import-controller.d.ts +4 -0
  67. package/dist/csv/import-controller.js +189 -169
  68. package/dist/csv/match-headers.js +29 -22
  69. package/dist/csv/parse-with.js +60 -54
  70. package/dist/csv/run-import.js +69 -72
  71. package/dist/csv/validate-rows.d.ts +1 -1
  72. package/dist/csv/validate-rows.js +65 -52
  73. package/dist/data/cache-ops.d.ts +5 -4
  74. package/dist/data/cache-ops.js +208 -188
  75. package/dist/data/combine-data-providers.js +25 -23
  76. package/dist/data/data-provider-log.d.ts +45 -0
  77. package/dist/data/data-provider-log.js +93 -0
  78. package/dist/data/errors.js +14 -10
  79. package/dist/data/fake-provider.d.ts +6 -3
  80. package/dist/data/fake-provider.js +135 -114
  81. package/dist/data/fetch-utils.js +61 -44
  82. package/dist/data/filter-branch.d.ts +5 -0
  83. package/dist/data/filter-branch.js +10 -0
  84. package/dist/data/get-many-aggregate.js +84 -76
  85. package/dist/data/http-error.js +17 -12
  86. package/dist/data/keys.js +50 -36
  87. package/dist/data/mutation-options.d.ts +14 -9
  88. package/dist/data/mutation-options.js +93 -109
  89. package/dist/data/query-options.js +73 -68
  90. package/dist/data/remove-empty.d.ts +3 -0
  91. package/dist/data/remove-empty.js +11 -0
  92. package/dist/data/undoable.d.ts +2 -2
  93. package/dist/data/undoable.js +190 -174
  94. package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
  95. package/dist/data/with-lifecycle-callbacks.js +66 -52
  96. package/dist/environment/application-update-store.js +58 -55
  97. package/dist/environment/offline-store.js +21 -21
  98. package/dist/form/array-binding.d.ts +30 -11
  99. package/dist/form/array-binding.js +184 -166
  100. package/dist/form/choice-ids.d.ts +7 -0
  101. package/dist/form/choice-ids.js +11 -0
  102. package/dist/form/choices-controller.d.ts +22 -2
  103. package/dist/form/choices-controller.js +315 -275
  104. package/dist/form/date-format.d.ts +38 -0
  105. package/dist/form/date-format.js +89 -0
  106. package/dist/form/dirty-values.d.ts +2 -2
  107. package/dist/form/dirty-values.js +22 -11
  108. package/dist/form/duration.d.ts +22 -0
  109. package/dist/form/duration.js +49 -0
  110. package/dist/form/edit-create-form.d.ts +12 -5
  111. package/dist/form/edit-create-form.js +243 -188
  112. package/dist/form/event-value.d.ts +6 -0
  113. package/dist/form/event-value.js +17 -0
  114. package/dist/form/field-error.d.ts +14 -0
  115. package/dist/form/field-error.js +28 -0
  116. package/dist/form/field-name.d.ts +20 -0
  117. package/dist/form/field-name.js +6 -0
  118. package/dist/form/filter-form-values.js +63 -49
  119. package/dist/form/form-controller.d.ts +9 -1
  120. package/dist/form/form-controller.js +165 -160
  121. package/dist/form/form-groups.d.ts +4 -3
  122. package/dist/form/form-groups.js +79 -79
  123. package/dist/form/form-handle.d.ts +175 -0
  124. package/dist/form/form-handle.js +68 -0
  125. package/dist/form/input-binding.d.ts +56 -31
  126. package/dist/form/input-binding.js +171 -118
  127. package/dist/form/input-values.d.ts +140 -0
  128. package/dist/form/input-values.js +206 -0
  129. package/dist/form/json-text.d.ts +11 -0
  130. package/dist/form/json-text.js +62 -37
  131. package/dist/form/save-pipeline.d.ts +6 -8
  132. package/dist/form/save-pipeline.js +91 -82
  133. package/dist/form/set-form-values.d.ts +6 -0
  134. package/dist/form/set-form-values.js +10 -0
  135. package/dist/form/source.js +97 -65
  136. package/dist/form/text-of.d.ts +2 -0
  137. package/dist/form/text-of.js +7 -0
  138. package/dist/form/validators.d.ts +58 -19
  139. package/dist/form/validators.js +97 -71
  140. package/dist/geo/bbox.d.ts +5 -4
  141. package/dist/geo/bbox.js +69 -48
  142. package/dist/geo/bounds.d.ts +14 -3
  143. package/dist/geo/bounds.js +98 -68
  144. package/dist/geo/constraints.js +88 -93
  145. package/dist/geo/coordinates.d.ts +10 -0
  146. package/dist/geo/coordinates.js +44 -23
  147. package/dist/geo/editor.d.ts +13 -13
  148. package/dist/geo/editor.js +176 -153
  149. package/dist/geo/geocoding.d.ts +4 -4
  150. package/dist/geo/geocoding.js +38 -41
  151. package/dist/geo/geojson.d.ts +12 -74
  152. package/dist/geo/geojson.js +52 -40
  153. package/dist/geo/geometry-math.d.ts +4 -4
  154. package/dist/geo/geometry-math.js +100 -93
  155. package/dist/geo/index.d.ts +9 -8
  156. package/dist/geo/map-layers.d.ts +42 -7
  157. package/dist/geo/map-layers.js +88 -62
  158. package/dist/geo/map-style.d.ts +12 -0
  159. package/dist/geo/map-style.js +43 -32
  160. package/dist/geo/messages.js +48 -45
  161. package/dist/geo/nominatim.js +88 -80
  162. package/dist/geo/osm-presets.d.ts +4 -3
  163. package/dist/geo/osm-presets.js +108 -86
  164. package/dist/geo/osm-tags.js +14 -12
  165. package/dist/geo/overpass.d.ts +14 -2
  166. package/dist/geo/overpass.js +83 -90
  167. package/dist/geo/shape-value.d.ts +9 -5
  168. package/dist/geo/shape-value.js +169 -146
  169. package/dist/geo/snap-to-roads.d.ts +3 -3
  170. package/dist/geo/snap-to-roads.js +55 -50
  171. package/dist/geo/throttle.js +36 -39
  172. package/dist/guards.d.ts +44 -0
  173. package/dist/guards.js +77 -0
  174. package/dist/i18n/default-messages.js +34 -29
  175. package/dist/i18n/english-messages.d.ts +172 -0
  176. package/dist/i18n/english-messages.js +359 -189
  177. package/dist/i18n/i18n.d.ts +2 -2
  178. package/dist/i18n/i18n.js +47 -60
  179. package/dist/i18n/inflection.d.ts +34 -0
  180. package/dist/i18n/inflection.js +82 -0
  181. package/dist/i18n/messages.d.ts +10 -2
  182. package/dist/i18n/messages.js +17 -4
  183. package/dist/i18n/missing-keys.d.ts +4 -0
  184. package/dist/i18n/missing-keys.js +70 -62
  185. package/dist/i18n/translatable.d.ts +16 -1
  186. package/dist/i18n/translatable.js +40 -47
  187. package/dist/index.d.ts +87 -39
  188. package/dist/index.js +54 -301
  189. package/dist/json/deep-equal.d.ts +8 -0
  190. package/dist/json/deep-equal.js +24 -0
  191. package/dist/json/strip-json-comments.js +38 -42
  192. package/dist/noop.d.ts +2 -0
  193. package/dist/noop.js +4 -0
  194. package/dist/notification/error-message.d.ts +2 -0
  195. package/dist/notification/error-message.js +10 -0
  196. package/dist/notification/notification-store.d.ts +3 -2
  197. package/dist/notification/notification-store.js +41 -45
  198. package/dist/onboarding-tour/placement.d.ts +2 -0
  199. package/dist/onboarding-tour/placement.js +65 -78
  200. package/dist/onboarding-tour/tour-controller.js +46 -54
  201. package/dist/realtime/add-events-for-mutations.js +75 -81
  202. package/dist/realtime/announce-lock.js +16 -13
  203. package/dist/realtime/broadcast-channel-transport.js +65 -59
  204. package/dist/realtime/copy-members.js +22 -14
  205. package/dist/realtime/in-memory-lock-provider.js +40 -48
  206. package/dist/realtime/live.js +95 -101
  207. package/dist/realtime/lock-options.js +16 -16
  208. package/dist/realtime/lock-session.js +91 -78
  209. package/dist/realtime/memory-transport.js +25 -25
  210. package/dist/realtime/presence.js +120 -122
  211. package/dist/realtime/realtime-data-provider.d.ts +10 -0
  212. package/dist/realtime/realtime-data-provider.js +45 -42
  213. package/dist/realtime/reconnect-backoff.js +17 -0
  214. package/dist/realtime/sse-transport.d.ts +31 -0
  215. package/dist/realtime/sse-transport.js +235 -0
  216. package/dist/realtime/status-store.js +23 -28
  217. package/dist/realtime/topic-hub.js +62 -71
  218. package/dist/realtime/topics.js +10 -12
  219. package/dist/realtime/types.js +10 -12
  220. package/dist/realtime/unread-count.js +21 -21
  221. package/dist/realtime/websocket-transport.d.ts +32 -0
  222. package/dist/realtime/websocket-transport.js +306 -0
  223. package/dist/resource/describe-resource.d.ts +2 -0
  224. package/dist/resource/describe-resource.js +247 -219
  225. package/dist/resource/get-at-path.js +29 -14
  226. package/dist/resource/identifier.d.ts +18 -0
  227. package/dist/resource/identifier.js +27 -0
  228. package/dist/resource/record.d.ts +4 -0
  229. package/dist/resource/record.js +10 -0
  230. package/dist/resource/register.d.ts +7 -0
  231. package/dist/resource/registry-store.d.ts +45 -15
  232. package/dist/resource/registry-store.js +67 -60
  233. package/dist/resource/registry.d.ts +10 -0
  234. package/dist/resource/registry.js +8 -4
  235. package/dist/resource/source-path.d.ts +23 -2
  236. package/dist/rich-text/rich-text-extras.d.ts +134 -0
  237. package/dist/rich-text/rich-text-extras.js +181 -0
  238. package/dist/rich-text/rich-text-image.d.ts +124 -0
  239. package/dist/rich-text/rich-text-image.js +163 -0
  240. package/dist/rich-text/rich-text.d.ts +6 -0
  241. package/dist/rich-text/rich-text.js +125 -88
  242. package/dist/router/location.d.ts +24 -4
  243. package/dist/router/location.js +43 -18
  244. package/dist/router/match.js +47 -39
  245. package/dist/router/paths.js +40 -34
  246. package/dist/router/record-from-location.d.ts +1 -1
  247. package/dist/router/record-from-location.js +25 -17
  248. package/dist/store/adapters.js +51 -55
  249. package/dist/store/micro-store.js +57 -59
  250. package/dist/store/shallow-equal.d.ts +12 -0
  251. package/dist/store/shallow-equal.js +20 -0
  252. package/dist/store/store.js +54 -57
  253. package/dist/ui-defaults.d.ts +15 -0
  254. package/dist/ui-defaults.js +17 -0
  255. package/package.json +13 -11
@@ -1,103 +1,112 @@
1
+ import { isRecord } from "../guards.js";
2
+ // src/controller/list-params.ts
1
3
  function isSortOrder(value) {
2
- return value === "ASC" || value === "DESC";
4
+ return value === "ASC" || value === "DESC";
3
5
  }
6
+ /**
7
+ * Serialize URL-shaped list params into a query string (with a leading `?`,
8
+ * or `''` when no field is set). Only defined fields are emitted; `filter` and
9
+ * `displayedFilters` are JSON-encoded (an empty object serializes as `{}`).
10
+ */
4
11
  function serializeListParams(params) {
5
- const search = new URLSearchParams();
6
- if (params.filter !== void 0) {
7
- search.set("filter", JSON.stringify(params.filter));
8
- }
9
- if (params.displayedFilters !== void 0) {
10
- search.set("displayedFilters", JSON.stringify(params.displayedFilters));
11
- }
12
- if (params.sort !== void 0) {
13
- search.set("sort", params.sort);
14
- }
15
- if (params.order !== void 0) {
16
- search.set("order", params.order);
17
- }
18
- if (params.page !== void 0) {
19
- search.set("page", String(params.page));
20
- }
21
- if (params.perPage !== void 0) {
22
- search.set("perPage", String(params.perPage));
23
- }
24
- const encoded = search.toString();
25
- return encoded ? `?${encoded}` : "";
12
+ const search = new URLSearchParams();
13
+ if (params.filter !== void 0) search.set("filter", JSON.stringify(params.filter));
14
+ if (params.displayedFilters !== void 0) search.set("displayedFilters", JSON.stringify(params.displayedFilters));
15
+ if (params.sort !== void 0) search.set("sort", params.sort);
16
+ if (params.order !== void 0) search.set("order", params.order);
17
+ if (params.page !== void 0) search.set("page", String(params.page));
18
+ if (params.perPage !== void 0) search.set("perPage", String(params.perPage));
19
+ const encoded = search.toString();
20
+ return encoded ? `?${encoded}` : "";
26
21
  }
22
+ /**
23
+ * Parse a JSON-encoded object param; returns `undefined` on absence, bad JSON,
24
+ * or JSON that is not a plain object (`null`, an array, a primitive).
25
+ */
27
26
  function parseJsonObjectParam(raw) {
28
- if (raw === null) return void 0;
29
- try {
30
- const parsed = JSON.parse(raw);
31
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
32
- return void 0;
33
- return parsed;
34
- } catch {
35
- return void 0;
36
- }
27
+ if (raw === null) return void 0;
28
+ try {
29
+ const parsed = JSON.parse(raw);
30
+ if (!isRecord(parsed)) return void 0;
31
+ return parsed;
32
+ } catch {
33
+ return;
34
+ }
37
35
  }
36
+ /** Parse a positive integer param; returns `undefined` on absence, non-numeric input, or a value below 1. */
38
37
  function parsePositiveIntParam(raw) {
39
- if (raw === null) return void 0;
40
- const parsed = Number.parseInt(raw, 10);
41
- return parsed >= 1 ? parsed : void 0;
38
+ if (raw === null) return void 0;
39
+ const parsed = Number.parseInt(raw, 10);
40
+ return parsed >= 1 ? parsed : void 0;
42
41
  }
42
+ /**
43
+ * Parse a query string (with or without a leading `?`) into URL-shaped list
44
+ * params. Malformed values are ignored, never trusted or thrown: bad JSON, or
45
+ * JSON that is not an object, on `filter`/`displayedFilters`; a non-numeric
46
+ * `page`/`perPage`, or one below 1; and an `order` outside `ASC`/`DESC` are
47
+ * simply omitted. `order` is upper-cased first.
48
+ */
43
49
  function parseListParams(search) {
44
- const params = new URLSearchParams(
45
- search.startsWith("?") ? search.slice(1) : search
46
- );
47
- const result = {};
48
- const filter = parseJsonObjectParam(params.get("filter"));
49
- if (filter !== void 0) result.filter = filter;
50
- const displayedFilters = parseJsonObjectParam(
51
- params.get("displayedFilters")
52
- );
53
- if (displayedFilters !== void 0)
54
- result.displayedFilters = displayedFilters;
55
- const sort = params.get("sort");
56
- if (sort !== null) result.sort = sort;
57
- const rawOrder = params.get("order");
58
- if (rawOrder !== null) {
59
- const order = rawOrder.toUpperCase();
60
- if (isSortOrder(order)) result.order = order;
61
- }
62
- const page = parsePositiveIntParam(params.get("page"));
63
- if (page !== void 0) result.page = page;
64
- const perPage = parsePositiveIntParam(params.get("perPage"));
65
- if (perPage !== void 0) result.perPage = perPage;
66
- return result;
50
+ const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
51
+ const result = {};
52
+ const filter = parseJsonObjectParam(params.get("filter"));
53
+ if (filter !== void 0) result.filter = filter;
54
+ const displayedFilters = parseJsonObjectParam(params.get("displayedFilters"));
55
+ if (displayedFilters !== void 0) result.displayedFilters = displayedFilters;
56
+ const sort = params.get("sort");
57
+ if (sort !== null) result.sort = sort;
58
+ const rawOrder = params.get("order");
59
+ if (rawOrder !== null) {
60
+ const order = rawOrder.toUpperCase();
61
+ if (isSortOrder(order)) result.order = order;
62
+ }
63
+ const page = parsePositiveIntParam(params.get("page"));
64
+ if (page !== void 0) result.page = page;
65
+ const perPage = parsePositiveIntParam(params.get("perPage"));
66
+ if (perPage !== void 0) result.perPage = perPage;
67
+ return result;
67
68
  }
69
+ /**
70
+ * Whether the user has set any non-default list param — a filter with keys, an
71
+ * explicit sort/order, a page other than 1, or a perPage. Used by adapters to
72
+ * decide whether to seed the URL/store from these params or fall back to option
73
+ * defaults. Mirrors react-admin's `hasCustomParams` semantics (an empty filter
74
+ * and `page === 1` are not "custom"), without its filter-must-be-present guard.
75
+ */
68
76
  function hasCustomParams(params) {
69
- return params.filter !== void 0 && Object.keys(params.filter).length > 0 || params.sort !== void 0 || params.order !== void 0 || params.page !== void 0 && params.page !== 1 || params.perPage !== void 0;
77
+ return params.filter !== void 0 && Object.keys(params.filter).length > 0 || params.sort !== void 0 || params.order !== void 0 || params.page !== void 0 && params.page !== 1 || params.perPage !== void 0;
70
78
  }
79
+ /** The query string for a list's state; the primary sort becomes `sort`/`order`. */
71
80
  function serializeListState(state) {
72
- const primary = state.sort[0];
73
- return serializeListParams({
74
- filter: state.filter,
75
- displayedFilters: state.displayedFilters,
76
- sort: primary?.field,
77
- order: primary?.order,
78
- page: state.page,
79
- perPage: state.perPage
80
- });
81
+ const primary = state.sort[0];
82
+ return serializeListParams({
83
+ filter: state.filter,
84
+ displayedFilters: state.displayedFilters,
85
+ sort: primary?.field,
86
+ order: primary?.order,
87
+ page: state.page,
88
+ perPage: state.perPage
89
+ });
81
90
  }
91
+ /**
92
+ * The params a URL query string sets, as one `setParams` patch, or `null` when
93
+ * it sets none of {@link hasCustomParams}, so a clean URL keeps the option and
94
+ * store defaults. Shown filter inputs without a filter clear the filter.
95
+ */
82
96
  function listPatchFromSearch(search) {
83
- const query = parseListParams(search);
84
- if (!hasCustomParams(query)) return null;
85
- const patch = {};
86
- if (query.filter !== void 0 || query.displayedFilters !== void 0) {
87
- patch.filter = query.filter ?? {};
88
- patch.displayedFilters = query.displayedFilters;
89
- }
90
- if (query.sort !== void 0) {
91
- patch.sort = { field: query.sort, order: query.order ?? "ASC" };
92
- }
93
- if (query.perPage !== void 0) patch.perPage = query.perPage;
94
- if (query.page !== void 0) patch.page = query.page;
95
- return patch;
97
+ const query = parseListParams(search);
98
+ if (!hasCustomParams(query)) return null;
99
+ const patch = {};
100
+ if (query.filter !== void 0 || query.displayedFilters !== void 0) {
101
+ patch.filter = query.filter ?? {};
102
+ patch.displayedFilters = query.displayedFilters;
103
+ }
104
+ if (query.sort !== void 0) patch.sort = {
105
+ field: query.sort,
106
+ order: query.order ?? "ASC"
107
+ };
108
+ if (query.perPage !== void 0) patch.perPage = query.perPage;
109
+ if (query.page !== void 0) patch.page = query.page;
110
+ return patch;
96
111
  }
97
- export {
98
- hasCustomParams,
99
- listPatchFromSearch,
100
- parseListParams,
101
- serializeListParams,
102
- serializeListState
103
- };
112
+ export { hasCustomParams, listPatchFromSearch, parseListParams, serializeListParams, serializeListState };
@@ -3,13 +3,13 @@
3
3
  * react, no vue: both PivotGrid adapters build the same table from the same
4
4
  * records and print numbers the same way.
5
5
  */
6
- import type { SourcePath } from "../resource/source-path.js";
6
+ import type { SourceProp } from "../resource/source-path.js";
7
7
  export type PivotAggregator = "count" | "sum" | "avg" | "min" | "max";
8
8
  export interface PivotOptions<R> {
9
- rowSource: SourcePath<R>;
10
- columnSource: SourcePath<R>;
9
+ rowSource: SourceProp<R>;
10
+ columnSource: SourceProp<R>;
11
11
  /** Read by every aggregator but `count`. */
12
- valueSource?: SourcePath<R>;
12
+ valueSource?: SourceProp<R>;
13
13
  /** Defaults to `count`. */
14
14
  aggregator?: PivotAggregator;
15
15
  }
@@ -1,105 +1,99 @@
1
1
  import { getAtPath } from "../resource/get-at-path.js";
2
- const collator = new Intl.Collator(void 0, { numeric: true });
3
- const NUMERIC_KEY = /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i;
4
- const compareKeys = (a, b) => {
5
- if (a === b) return 0;
6
- if (a === "") return 1;
7
- if (b === "") return -1;
8
- if (NUMERIC_KEY.test(a) && NUMERIC_KEY.test(b)) {
9
- const difference = Number(a) - Number(b);
10
- if (difference !== 0 && !Number.isNaN(difference)) return difference;
11
- }
12
- return collator.compare(a, b) || (a < b ? -1 : 1);
2
+ // src/controller/pivot-grid.ts
3
+ /**
4
+ * Pure rule for pivoting a flat record list into a row by column table. No
5
+ * react, no vue: both PivotGrid adapters build the same table from the same
6
+ * records and print numbers the same way.
7
+ */
8
+ var collator = new Intl.Collator(void 0, { numeric: true });
9
+ var NUMERIC_KEY = /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i;
10
+ var compareKeys = (a, b) => {
11
+ if (a === b) return 0;
12
+ if (a === "") return 1;
13
+ if (b === "") return -1;
14
+ if (NUMERIC_KEY.test(a) && NUMERIC_KEY.test(b)) {
15
+ const difference = Number(a) - Number(b);
16
+ if (difference !== 0 && !Number.isNaN(difference)) return difference;
17
+ }
18
+ return collator.compare(a, b) || (a < b ? -1 : 1);
13
19
  };
14
- const toKey = (value) => value == null || value === "" ? "" : String(value);
15
- const toNumber = (value) => {
16
- if (typeof value === "number")
17
- return Number.isFinite(value) ? value : void 0;
18
- if (typeof value === "string" && value !== "") {
19
- const parsed = Number(value);
20
- return Number.isFinite(parsed) ? parsed : void 0;
21
- }
22
- return void 0;
20
+ var toKey = (value) => value == null || value === "" ? "" : String(value);
21
+ var toNumber = (value) => {
22
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
23
+ if (typeof value === "string" && value !== "") {
24
+ const parsed = Number(value);
25
+ return Number.isFinite(parsed) ? parsed : void 0;
26
+ }
23
27
  };
24
- const aggregate = (group, aggregator) => {
25
- if (group.count === 0) return null;
26
- if (aggregator === "count") return group.count;
27
- const { values } = group;
28
- if (values.length === 0) return null;
29
- switch (aggregator) {
30
- case "sum":
31
- return values.reduce((total, value) => total + value, 0);
32
- case "avg":
33
- return values.reduce((total, value) => total + value, 0) / values.length;
34
- case "min":
35
- return values.reduce((a, b) => b < a ? b : a);
36
- case "max":
37
- return values.reduce((a, b) => b > a ? b : a);
38
- }
28
+ var aggregate = (group, aggregator) => {
29
+ if (group.count === 0) return null;
30
+ if (aggregator === "count") return group.count;
31
+ const { values } = group;
32
+ if (values.length === 0) return null;
33
+ switch (aggregator) {
34
+ case "sum": return values.reduce((total, value) => total + value, 0);
35
+ case "avg": return values.reduce((total, value) => total + value, 0) / values.length;
36
+ case "min": return values.reduce((a, b) => b < a ? b : a);
37
+ case "max": return values.reduce((a, b) => b > a ? b : a);
38
+ }
39
39
  };
40
- const newGroup = () => ({ count: 0, values: [] });
41
- const getOrCreate = (map, key, create) => {
42
- const existing = map.get(key);
43
- if (existing !== void 0) return existing;
44
- const created = create();
45
- map.set(key, created);
46
- return created;
40
+ var newGroup = () => ({
41
+ count: 0,
42
+ values: []
43
+ });
44
+ var getOrCreate = (map, key, create) => {
45
+ const existing = map.get(key);
46
+ if (existing !== void 0) return existing;
47
+ const created = create();
48
+ map.set(key, created);
49
+ return created;
47
50
  };
48
- const add = (group, value) => {
49
- group.count += 1;
50
- if (value !== void 0) group.values.push(value);
51
+ var add = (group, value) => {
52
+ group.count += 1;
53
+ if (value !== void 0) group.values.push(value);
51
54
  };
55
+ /**
56
+ * A row or column key is `String(value)`, and a null, undefined or empty value
57
+ * is the key `""`. Keys sort numerically (`10` after `9`) with `""` last.
58
+ * `count` counts records and ignores `valueSource`. The other aggregators use
59
+ * finite numbers and numeric strings, and skip anything else. A cell with no
60
+ * records is `null`, and so is a cell with no usable value, unless it counts.
61
+ * A total aggregates the raw values of its row, column or table, never the
62
+ * cell results, so an average total is not a sum of averages.
63
+ */
52
64
  function buildPivotTable(records, options) {
53
- const {
54
- rowSource,
55
- columnSource,
56
- valueSource,
57
- aggregator = "count"
58
- } = options;
59
- const readsValue = aggregator !== "count" && valueSource !== void 0;
60
- const cellGroups = /* @__PURE__ */ new Map();
61
- const rowGroups = /* @__PURE__ */ new Map();
62
- const columnGroups = /* @__PURE__ */ new Map();
63
- const tableGroup = newGroup();
64
- for (const record of records) {
65
- const row = toKey(getAtPath(record, rowSource));
66
- const column = toKey(getAtPath(record, columnSource));
67
- const value = readsValue ? toNumber(getAtPath(record, valueSource)) : void 0;
68
- const cellRow = getOrCreate(cellGroups, row, () => /* @__PURE__ */ new Map());
69
- const cell = getOrCreate(cellRow, column, newGroup);
70
- const rowGroup = getOrCreate(rowGroups, row, newGroup);
71
- const columnGroup = getOrCreate(columnGroups, column, newGroup);
72
- add(cell, value);
73
- add(rowGroup, value);
74
- add(columnGroup, value);
75
- add(tableGroup, value);
76
- }
77
- const rowKeys = [...rowGroups.keys()].sort(compareKeys);
78
- const columnKeys = [...columnGroups.keys()].sort(compareKeys);
79
- const empty = newGroup();
80
- return {
81
- rowKeys,
82
- columnKeys,
83
- cells: rowKeys.map(
84
- (row) => columnKeys.map(
85
- (column) => aggregate(cellGroups.get(row)?.get(column) ?? empty, aggregator)
86
- )
87
- ),
88
- rowTotals: rowKeys.map(
89
- (row) => aggregate(rowGroups.get(row), aggregator)
90
- ),
91
- columnTotals: columnKeys.map(
92
- (column) => aggregate(columnGroups.get(column), aggregator)
93
- ),
94
- grandTotal: aggregate(tableGroup, aggregator)
95
- };
65
+ const { rowSource, columnSource, valueSource, aggregator = "count" } = options;
66
+ const readsValue = aggregator !== "count" && valueSource !== void 0;
67
+ const cellGroups = /* @__PURE__ */ new Map();
68
+ const rowGroups = /* @__PURE__ */ new Map();
69
+ const columnGroups = /* @__PURE__ */ new Map();
70
+ const tableGroup = newGroup();
71
+ for (const record of records) {
72
+ const row = toKey(getAtPath(record, rowSource));
73
+ const column = toKey(getAtPath(record, columnSource));
74
+ const value = readsValue ? toNumber(getAtPath(record, valueSource)) : void 0;
75
+ const cell = getOrCreate(getOrCreate(cellGroups, row, () => /* @__PURE__ */ new Map()), column, newGroup);
76
+ const rowGroup = getOrCreate(rowGroups, row, newGroup);
77
+ const columnGroup = getOrCreate(columnGroups, column, newGroup);
78
+ add(cell, value);
79
+ add(rowGroup, value);
80
+ add(columnGroup, value);
81
+ add(tableGroup, value);
82
+ }
83
+ const rowKeys = [...rowGroups.keys()].sort(compareKeys);
84
+ const columnKeys = [...columnGroups.keys()].sort(compareKeys);
85
+ const empty = newGroup();
86
+ return {
87
+ rowKeys,
88
+ columnKeys,
89
+ cells: rowKeys.map((row) => columnKeys.map((column) => aggregate(cellGroups.get(row)?.get(column) ?? empty, aggregator))),
90
+ rowTotals: rowKeys.map((row) => aggregate(rowGroups.get(row), aggregator)),
91
+ columnTotals: columnKeys.map((column) => aggregate(columnGroups.get(column), aggregator)),
92
+ grandTotal: aggregate(tableGroup, aggregator)
93
+ };
96
94
  }
95
+ /** Integers print bare and fractions round to two digits. */
97
96
  function formatPivotValue(value, locale) {
98
- return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(
99
- value
100
- );
97
+ return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(value);
101
98
  }
102
- export {
103
- buildPivotTable,
104
- formatPivotValue
105
- };
99
+ export { buildPivotTable, formatPivotValue };
@@ -4,7 +4,7 @@
4
4
  * and the cached page, then asks these functions what to fetch and who the
5
5
  * neighbours are.
6
6
  */
7
- import type { FilterPayload, GetListParams, GetListResult, SortPayload } from "../data/types.js";
7
+ import type { FilterPayload, GetListParams, GetListResult, InfiniteListParams, SortPayload } from "../data/types.js";
8
8
  import type { Identifier } from "../resource/identifier.js";
9
9
  /** The list's persisted params, read from the `<resource>.listParams` store key. */
10
10
  export interface PrevNextStoredParams {
@@ -31,7 +31,7 @@ export interface PrevNextOptions {
31
31
  }
32
32
  export interface PrevNextParams {
33
33
  /** The page the list itself showed: the key its query cached it under. */
34
- cached: GetListParams;
34
+ cached: InfiniteListParams;
35
35
  /** The first `limit` rows, for when the cached page cannot serve. */
36
36
  fetch: GetListParams;
37
37
  }
@@ -1,62 +1,82 @@
1
- const DEFAULT_PREV_NEXT_LIMIT = 1e3;
2
- const DEFAULT_PER_PAGE = 10;
3
- const DEFAULT_SORT = { field: "id", order: "ASC" };
1
+ import "./list-controller.js";
2
+ // src/controller/prev-next.ts
3
+ var DEFAULT_PREV_NEXT_LIMIT = 1e3;
4
+ var DEFAULT_SORT = {
5
+ field: "id",
6
+ order: "ASC"
7
+ };
8
+ /**
9
+ * A list keys its query with a single sort object when there is exactly one
10
+ * sort key, so the cached page is found only under that shape.
11
+ */
4
12
  function toListSort(sort) {
5
- if (Array.isArray(sort)) {
6
- return sort.length === 1 ? { ...sort[0] } : sort;
7
- }
8
- return sort ?? { ...DEFAULT_SORT };
13
+ if (Array.isArray(sort)) {
14
+ const [only] = sort;
15
+ return sort.length === 1 && only ? { ...only } : sort;
16
+ }
17
+ return sort ?? { ...DEFAULT_SORT };
9
18
  }
10
19
  function getPrevNextParams(stored, options = {}) {
11
- const persisted = options.storeKey === false ? void 0 : stored;
12
- const page = persisted?.page ?? 1;
13
- const perPage = persisted?.perPage ?? DEFAULT_PER_PAGE;
14
- const sort = toListSort(
15
- persisted?.sort && (!Array.isArray(persisted.sort) || persisted.sort.length) ? persisted.sort : options.sort
16
- );
17
- const filter = {
18
- ...persisted?.filter ?? options.filterDefaultValues,
19
- ...options.filter
20
- };
21
- return {
22
- cached: { pagination: { page, perPage }, sort, filter },
23
- fetch: {
24
- pagination: {
25
- page: 1,
26
- perPage: options.limit ?? DEFAULT_PREV_NEXT_LIMIT
27
- },
28
- sort,
29
- filter,
30
- meta: options.meta
31
- }
32
- };
20
+ const persisted = options.storeKey === false ? void 0 : stored;
21
+ const page = persisted?.page ?? 1;
22
+ const perPage = persisted?.perPage ?? 10;
23
+ const sort = toListSort(persisted?.sort && (!Array.isArray(persisted.sort) || persisted.sort.length) ? persisted.sort : options.sort);
24
+ const filter = {
25
+ ...persisted?.filter ?? options.filterDefaultValues,
26
+ ...options.filter
27
+ };
28
+ return {
29
+ cached: {
30
+ pagination: {
31
+ page,
32
+ perPage
33
+ },
34
+ sort,
35
+ filter
36
+ },
37
+ fetch: {
38
+ pagination: {
39
+ page: 1,
40
+ perPage: options.limit ?? 1e3
41
+ },
42
+ sort,
43
+ filter,
44
+ meta: options.meta
45
+ }
46
+ };
33
47
  }
48
+ /**
49
+ * Whether the page a list cached holds both neighbours of `id`. The first row
50
+ * of a later page has its previous record on the page before, and the last row
51
+ * of an earlier page has its next one on the page after.
52
+ */
34
53
  function canUseCachedPage(id, cached, page, perPage) {
35
- if (!cached) return false;
36
- const index = cached.data.findIndex((record) => record.id === id);
37
- if (index === -1) return false;
38
- if (index === 0 && page > 1) return false;
39
- if (index === cached.data.length - 1) {
40
- const hasMore = cached.total != null ? page * perPage < cached.total : cached.pageInfo?.hasNextPage ?? false;
41
- if (hasMore) return false;
42
- }
43
- return true;
54
+ if (!cached) return false;
55
+ const index = cached.data.findIndex((record) => record.id === id);
56
+ if (index === -1) return false;
57
+ if (index === 0 && page > 1) return false;
58
+ if (index === cached.data.length - 1) {
59
+ if (cached.total != null ? page * perPage < cached.total : cached.pageInfo?.hasNextPage ?? false) return false;
60
+ }
61
+ return true;
44
62
  }
63
+ /**
64
+ * The neighbours of `id` among `records`. `offset` is the number of rows that
65
+ * precede `records` in the whole list, so `index` is a position in that list.
66
+ */
45
67
  function resolvePrevNext(id, records, total, offset) {
46
- const position = records.findIndex((record) => record.id === id);
47
- if (position === -1) {
48
- return { prevId: null, nextId: null, index: void 0, total };
49
- }
50
- return {
51
- prevId: records[position - 1]?.id ?? null,
52
- nextId: records[position + 1]?.id ?? null,
53
- index: position + offset,
54
- total
55
- };
68
+ const position = records.findIndex((record) => record.id === id);
69
+ if (position === -1) return {
70
+ prevId: null,
71
+ nextId: null,
72
+ index: void 0,
73
+ total
74
+ };
75
+ return {
76
+ prevId: records[position - 1]?.id ?? null,
77
+ nextId: records[position + 1]?.id ?? null,
78
+ index: position + offset,
79
+ total
80
+ };
56
81
  }
57
- export {
58
- DEFAULT_PREV_NEXT_LIMIT,
59
- canUseCachedPage,
60
- getPrevNextParams,
61
- resolvePrevNext
62
- };
82
+ export { DEFAULT_PREV_NEXT_LIMIT, canUseCachedPage, getPrevNextParams, resolvePrevNext };
@@ -2,6 +2,7 @@ import { type MutationMode } from "../data/mutation-options.js";
2
2
  import { type DataContext } from "../data/query-options.js";
3
3
  import type { NotificationStore } from "../notification/notification-store.js";
4
4
  import type { Identifier } from "../resource/identifier.js";
5
+ import type { ResourceRecord } from "../resource/record.js";
5
6
  import type { RecordOf, ResourceMap } from "../resource/registry.js";
6
7
  import { type MicroStore } from "../store/micro-store.js";
7
8
  import type { CheloneStore } from "../store/store.js";
@@ -9,7 +10,7 @@ export interface ShowControllerState<TRecord> {
9
10
  record: TRecord | undefined;
10
11
  isPending: boolean;
11
12
  isFetching: boolean;
12
- error: unknown;
13
+ error: Error | null;
13
14
  }
14
15
  export interface EditControllerState<TRecord> extends ShowControllerState<TRecord> {
15
16
  saving: boolean;
@@ -19,7 +20,7 @@ export type RedirectTarget = "list" | "edit" | "show" | "create";
19
20
  * Controllers NEVER touch a router or window — navigation intent surfaces as
20
21
  * this callback and the adapter (or app) decides what to do with it.
21
22
  */
22
- export type RedirectRequest = (target: RedirectTarget, resource: string, id?: Identifier, data?: unknown) => void;
23
+ export type RedirectRequest = (target: RedirectTarget, resource: string, id?: Identifier, data?: ResourceRecord) => void;
23
24
  export interface RecordControllerContext<TResources extends ResourceMap = ResourceMap> extends DataContext<TResources> {
24
25
  notifications?: NotificationStore;
25
26
  translate?: (key: string) => string;
@@ -39,17 +40,24 @@ export type SaveTransform<TRecord> = (data: Partial<TRecord>, extra?: {
39
40
  previousData?: TRecord;
40
41
  }) => Partial<TRecord> | Promise<Partial<TRecord>>;
41
42
  export interface SaveOptions<TRecord> {
42
- /** Call-time transform — takes priority over the controller-level one. */
43
- transform?: SaveTransform<TRecord>;
43
+ /**
44
+ * Call-time transform — takes priority over the controller-level one.
45
+ * Declared as a method (like `onSaved` and `onSuccess`) so `SaveOptions<Post>`
46
+ * stays assignable to `SaveOptions<ResourceRecord>`: a view publishes its
47
+ * `save` into a context typed for the default record.
48
+ */
49
+ transform?(data: Partial<TRecord>, extra?: {
50
+ previousData?: TRecord;
51
+ }): Partial<TRecord> | Promise<Partial<TRecord>>;
44
52
  /**
45
53
  * OBSERVES the saved record WITHOUT replacing the default success handling
46
54
  * (runs before `onSuccess`/defaults). The P5 composed form controllers
47
55
  * reconcile form state with server truth through this — the save contract
48
56
  * itself resolves `FieldErrors | undefined`, never the record.
49
57
  */
50
- onSaved?: (record: TRecord) => void | Promise<void>;
58
+ onSaved?(record: TRecord): void | Promise<void>;
51
59
  /** REPLACES the default success handling (notify + redirect request). */
52
- onSuccess?: (record: TRecord) => void | Promise<void>;
60
+ onSuccess?(record: TRecord): void | Promise<void>;
53
61
  /** REPLACES the default error handling (the http_error notify). */
54
62
  onError?: (error: unknown) => void | Promise<void>;
55
63
  meta?: unknown;