@chelone/core 0.5.0 → 0.7.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 (253) hide show
  1. package/dist/assistant/assistant-session.d.ts +28 -0
  2. package/dist/assistant/assistant-session.js +208 -0
  3. package/dist/assistant/echo-transport.d.ts +7 -0
  4. package/dist/assistant/echo-transport.js +28 -0
  5. package/dist/assistant/http-transport.d.ts +10 -0
  6. package/dist/assistant/http-transport.js +98 -0
  7. package/dist/assistant/types.d.ts +54 -0
  8. package/dist/auth/access-control.d.ts +13 -5
  9. package/dist/auth/access-control.js +25 -21
  10. package/dist/auth/auth-options.d.ts +21 -17
  11. package/dist/auth/auth-options.js +80 -48
  12. package/dist/auth/permission-matrix.d.ts +12 -0
  13. package/dist/auth/permission-matrix.js +67 -0
  14. package/dist/auth/types.d.ts +14 -4
  15. package/dist/block-editor/block-document.d.ts +60 -0
  16. package/dist/block-editor/block-document.js +143 -0
  17. package/dist/block-editor/data-blocks.d.ts +63 -0
  18. package/dist/block-editor/data-blocks.js +117 -0
  19. package/dist/chart/chart-data.d.ts +25 -0
  20. package/dist/chart/chart-data.js +61 -0
  21. package/dist/command-menu/hotkey.js +58 -47
  22. package/dist/command-menu/recents.js +8 -11
  23. package/dist/command-menu/registry.d.ts +9 -5
  24. package/dist/command-menu/registry.js +36 -46
  25. package/dist/controller/calendar-dates.js +82 -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 +42 -0
  35. package/dist/controller/in-place-editor.js +42 -0
  36. package/dist/controller/infinite-list-controller.js +220 -190
  37. package/dist/controller/inspector.d.ts +49 -0
  38. package/dist/controller/inspector.js +64 -0
  39. package/dist/controller/job-monitor.d.ts +41 -0
  40. package/dist/controller/job-monitor.js +49 -0
  41. package/dist/controller/kanban-board.d.ts +2 -3
  42. package/dist/controller/kanban-board.js +43 -24
  43. package/dist/controller/list-controller.d.ts +5 -5
  44. package/dist/controller/list-controller.js +444 -400
  45. package/dist/controller/list-params.js +96 -87
  46. package/dist/controller/pivot-grid.d.ts +36 -0
  47. package/dist/controller/pivot-grid.js +99 -0
  48. package/dist/controller/prev-next.d.ts +2 -2
  49. package/dist/controller/prev-next.js +74 -54
  50. package/dist/controller/record-controllers.d.ts +14 -6
  51. package/dist/controller/record-controllers.js +288 -257
  52. package/dist/controller/record-diff.d.ts +21 -0
  53. package/dist/controller/record-diff.js +50 -0
  54. package/dist/controller/record-timeline.d.ts +35 -0
  55. package/dist/controller/record-timeline.js +48 -0
  56. package/dist/controller/record-tree.js +57 -47
  57. package/dist/controller/record-workflow.d.ts +38 -16
  58. package/dist/controller/record-workflow.js +85 -64
  59. package/dist/controller/reference-many-controller.js +198 -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 +2 -1
  65. package/dist/csv/export.js +43 -40
  66. package/dist/csv/import-controller.js +185 -169
  67. package/dist/csv/match-headers.js +29 -22
  68. package/dist/csv/parse-with.js +60 -54
  69. package/dist/csv/run-import.js +69 -72
  70. package/dist/csv/validate-rows.d.ts +1 -1
  71. package/dist/csv/validate-rows.js +65 -52
  72. package/dist/data/cache-ops.d.ts +5 -4
  73. package/dist/data/cache-ops.js +208 -190
  74. package/dist/data/combine-data-providers.js +25 -23
  75. package/dist/data/data-provider-log.d.ts +45 -0
  76. package/dist/data/data-provider-log.js +93 -0
  77. package/dist/data/errors.js +14 -10
  78. package/dist/data/fake-provider.d.ts +6 -3
  79. package/dist/data/fake-provider.js +129 -114
  80. package/dist/data/fetch-utils.js +61 -44
  81. package/dist/data/filter-branch.d.ts +5 -0
  82. package/dist/data/filter-branch.js +10 -0
  83. package/dist/data/get-many-aggregate.js +84 -76
  84. package/dist/data/http-error.js +17 -12
  85. package/dist/data/keys.js +50 -36
  86. package/dist/data/mutation-options.d.ts +14 -9
  87. package/dist/data/mutation-options.js +93 -109
  88. package/dist/data/query-options.js +73 -68
  89. package/dist/data/remove-empty.d.ts +3 -0
  90. package/dist/data/remove-empty.js +11 -0
  91. package/dist/data/undoable.d.ts +2 -2
  92. package/dist/data/undoable.js +190 -174
  93. package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
  94. package/dist/data/with-lifecycle-callbacks.js +66 -52
  95. package/dist/environment/application-update-store.js +58 -55
  96. package/dist/environment/offline-store.js +21 -21
  97. package/dist/form/array-binding.d.ts +30 -11
  98. package/dist/form/array-binding.js +184 -166
  99. package/dist/form/choice-ids.d.ts +7 -0
  100. package/dist/form/choice-ids.js +11 -0
  101. package/dist/form/choices-controller.d.ts +3 -2
  102. package/dist/form/choices-controller.js +302 -275
  103. package/dist/form/date-format.d.ts +38 -0
  104. package/dist/form/date-format.js +89 -0
  105. package/dist/form/dirty-values.d.ts +2 -2
  106. package/dist/form/dirty-values.js +22 -11
  107. package/dist/form/duration.d.ts +22 -0
  108. package/dist/form/duration.js +49 -0
  109. package/dist/form/edit-create-form.d.ts +12 -5
  110. package/dist/form/edit-create-form.js +243 -188
  111. package/dist/form/event-value.d.ts +6 -0
  112. package/dist/form/event-value.js +17 -0
  113. package/dist/form/field-error.d.ts +14 -0
  114. package/dist/form/field-error.js +28 -0
  115. package/dist/form/field-name.d.ts +20 -0
  116. package/dist/form/field-name.js +6 -0
  117. package/dist/form/filter-form-values.js +63 -49
  118. package/dist/form/form-controller.d.ts +9 -1
  119. package/dist/form/form-controller.js +165 -160
  120. package/dist/form/form-groups.d.ts +4 -3
  121. package/dist/form/form-groups.js +79 -79
  122. package/dist/form/form-handle.d.ts +175 -0
  123. package/dist/form/form-handle.js +68 -0
  124. package/dist/form/input-binding.d.ts +56 -31
  125. package/dist/form/input-binding.js +169 -118
  126. package/dist/form/input-values.d.ts +130 -0
  127. package/dist/form/input-values.js +177 -0
  128. package/dist/form/json-text.js +51 -37
  129. package/dist/form/save-pipeline.d.ts +6 -8
  130. package/dist/form/save-pipeline.js +91 -82
  131. package/dist/form/set-form-values.d.ts +6 -0
  132. package/dist/form/set-form-values.js +10 -0
  133. package/dist/form/source.js +97 -65
  134. package/dist/form/text-of.d.ts +2 -0
  135. package/dist/form/text-of.js +7 -0
  136. package/dist/form/validators.d.ts +58 -19
  137. package/dist/form/validators.js +97 -71
  138. package/dist/geo/bbox.d.ts +5 -4
  139. package/dist/geo/bbox.js +69 -48
  140. package/dist/geo/bounds.d.ts +14 -3
  141. package/dist/geo/bounds.js +98 -68
  142. package/dist/geo/constraints.js +88 -93
  143. package/dist/geo/coordinates.js +31 -23
  144. package/dist/geo/editor.d.ts +13 -13
  145. package/dist/geo/editor.js +176 -153
  146. package/dist/geo/geocoding.d.ts +4 -4
  147. package/dist/geo/geocoding.js +38 -41
  148. package/dist/geo/geojson.d.ts +12 -74
  149. package/dist/geo/geojson.js +52 -40
  150. package/dist/geo/geometry-math.d.ts +4 -4
  151. package/dist/geo/geometry-math.js +100 -93
  152. package/dist/geo/index.d.ts +7 -6
  153. package/dist/geo/map-layers.d.ts +42 -7
  154. package/dist/geo/map-layers.js +88 -62
  155. package/dist/geo/map-style.js +31 -32
  156. package/dist/geo/messages.js +48 -45
  157. package/dist/geo/nominatim.js +88 -80
  158. package/dist/geo/osm-presets.d.ts +4 -3
  159. package/dist/geo/osm-presets.js +108 -86
  160. package/dist/geo/osm-tags.js +14 -12
  161. package/dist/geo/overpass.d.ts +14 -2
  162. package/dist/geo/overpass.js +83 -90
  163. package/dist/geo/shape-value.d.ts +9 -5
  164. package/dist/geo/shape-value.js +169 -146
  165. package/dist/geo/snap-to-roads.d.ts +3 -3
  166. package/dist/geo/snap-to-roads.js +55 -50
  167. package/dist/geo/throttle.js +36 -39
  168. package/dist/guards.d.ts +37 -0
  169. package/dist/guards.js +62 -0
  170. package/dist/i18n/default-messages.js +34 -29
  171. package/dist/i18n/english-messages.js +187 -189
  172. package/dist/i18n/i18n.d.ts +8 -2
  173. package/dist/i18n/i18n.js +47 -48
  174. package/dist/i18n/inflection.d.ts +34 -0
  175. package/dist/i18n/inflection.js +82 -0
  176. package/dist/i18n/messages.d.ts +10 -2
  177. package/dist/i18n/messages.js +17 -4
  178. package/dist/i18n/missing-keys.d.ts +19 -0
  179. package/dist/i18n/missing-keys.js +68 -0
  180. package/dist/i18n/translatable.d.ts +16 -1
  181. package/dist/i18n/translatable.js +40 -47
  182. package/dist/index.d.ts +96 -22
  183. package/dist/index.js +52 -257
  184. package/dist/json/deep-equal.d.ts +8 -0
  185. package/dist/json/deep-equal.js +24 -0
  186. package/dist/json/strip-json-comments.js +38 -42
  187. package/dist/noop.d.ts +2 -0
  188. package/dist/noop.js +4 -0
  189. package/dist/notification/error-message.d.ts +2 -0
  190. package/dist/notification/error-message.js +10 -0
  191. package/dist/notification/notification-store.d.ts +3 -2
  192. package/dist/notification/notification-store.js +41 -45
  193. package/dist/onboarding-tour/placement.d.ts +34 -0
  194. package/dist/onboarding-tour/placement.js +67 -0
  195. package/dist/onboarding-tour/tour-controller.d.ts +25 -0
  196. package/dist/onboarding-tour/tour-controller.js +50 -0
  197. package/dist/realtime/add-events-for-mutations.js +75 -81
  198. package/dist/realtime/announce-lock.d.ts +7 -0
  199. package/dist/realtime/announce-lock.js +18 -0
  200. package/dist/realtime/broadcast-channel-transport.d.ts +14 -0
  201. package/dist/realtime/broadcast-channel-transport.js +69 -0
  202. package/dist/realtime/copy-members.js +22 -14
  203. package/dist/realtime/in-memory-lock-provider.js +41 -60
  204. package/dist/realtime/live.js +95 -101
  205. package/dist/realtime/lock-options.js +16 -16
  206. package/dist/realtime/lock-session.js +91 -78
  207. package/dist/realtime/memory-transport.js +26 -67
  208. package/dist/realtime/presence.js +120 -122
  209. package/dist/realtime/realtime-data-provider.d.ts +10 -0
  210. package/dist/realtime/realtime-data-provider.js +45 -42
  211. package/dist/realtime/reconnect-backoff.d.ts +15 -0
  212. package/dist/realtime/reconnect-backoff.js +17 -0
  213. package/dist/realtime/sse-transport.d.ts +31 -0
  214. package/dist/realtime/sse-transport.js +235 -0
  215. package/dist/realtime/status-store.js +23 -28
  216. package/dist/realtime/topic-hub.d.ts +24 -0
  217. package/dist/realtime/topic-hub.js +66 -0
  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 +232 -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 +16 -1
  236. package/dist/rich-text/rich-text-extras.d.ts +132 -0
  237. package/dist/rich-text/rich-text-extras.js +179 -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 +62 -0
  241. package/dist/rich-text/rich-text.js +142 -0
  242. package/dist/router/location.d.ts +22 -4
  243. package/dist/router/location.js +41 -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/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 };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pure rule for pivoting a flat record list into a row by column table. No
3
+ * react, no vue: both PivotGrid adapters build the same table from the same
4
+ * records and print numbers the same way.
5
+ */
6
+ import type { SourcePath } from "../resource/source-path.js";
7
+ export type PivotAggregator = "count" | "sum" | "avg" | "min" | "max";
8
+ export interface PivotOptions<R> {
9
+ rowSource: SourcePath<R>;
10
+ columnSource: SourcePath<R>;
11
+ /** Read by every aggregator but `count`. */
12
+ valueSource?: SourcePath<R>;
13
+ /** Defaults to `count`. */
14
+ aggregator?: PivotAggregator;
15
+ }
16
+ export interface PivotTable {
17
+ rowKeys: string[];
18
+ columnKeys: string[];
19
+ /** `cells[row][column]` follows the order of `rowKeys` and `columnKeys`. */
20
+ cells: (number | null)[][];
21
+ rowTotals: (number | null)[];
22
+ columnTotals: (number | null)[];
23
+ grandTotal: number | null;
24
+ }
25
+ /**
26
+ * A row or column key is `String(value)`, and a null, undefined or empty value
27
+ * is the key `""`. Keys sort numerically (`10` after `9`) with `""` last.
28
+ * `count` counts records and ignores `valueSource`. The other aggregators use
29
+ * finite numbers and numeric strings, and skip anything else. A cell with no
30
+ * records is `null`, and so is a cell with no usable value, unless it counts.
31
+ * A total aggregates the raw values of its row, column or table, never the
32
+ * cell results, so an average total is not a sum of averages.
33
+ */
34
+ export declare function buildPivotTable<R>(records: readonly R[], options: PivotOptions<R>): PivotTable;
35
+ /** Integers print bare and fractions round to two digits. */
36
+ export declare function formatPivotValue(value: number, locale?: string): string;
@@ -0,0 +1,99 @@
1
+ import { getAtPath } from "../resource/get-at-path.js";
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);
19
+ };
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
+ }
27
+ };
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
+ };
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;
50
+ };
51
+ var add = (group, value) => {
52
+ group.count += 1;
53
+ if (value !== void 0) group.values.push(value);
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
+ */
64
+ function buildPivotTable(records, options) {
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
+ };
94
+ }
95
+ /** Integers print bare and fractions round to two digits. */
96
+ function formatPivotValue(value, locale) {
97
+ return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(value);
98
+ }
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
+ // src/controller/prev-next.ts
2
+ var DEFAULT_PREV_NEXT_LIMIT = 1e3;
3
+ var DEFAULT_PER_PAGE = 10;
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 ?? DEFAULT_PER_PAGE;
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;