@chelone/core 0.6.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 (242) 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 +21 -17
  10. package/dist/auth/auth-options.js +80 -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 +60 -0
  15. package/dist/block-editor/block-document.js +143 -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.js +8 -11
  21. package/dist/command-menu/registry.d.ts +9 -5
  22. package/dist/command-menu/registry.js +36 -46
  23. package/dist/controller/calendar-dates.js +82 -113
  24. package/dist/controller/calendar-events.js +93 -88
  25. package/dist/controller/client-list-controller.d.ts +2 -2
  26. package/dist/controller/client-list-controller.js +251 -254
  27. package/dist/controller/data-table-columns.d.ts +2 -0
  28. package/dist/controller/data-table-columns.js +50 -56
  29. package/dist/controller/deep-equal.d.ts +1 -0
  30. package/dist/controller/deep-equal.js +14 -0
  31. package/dist/controller/filter-toggle.js +36 -44
  32. package/dist/controller/in-place-editor.d.ts +42 -0
  33. package/dist/controller/in-place-editor.js +42 -0
  34. package/dist/controller/infinite-list-controller.js +219 -189
  35. package/dist/controller/inspector.d.ts +49 -0
  36. package/dist/controller/inspector.js +64 -0
  37. package/dist/controller/job-monitor.d.ts +3 -4
  38. package/dist/controller/job-monitor.js +37 -47
  39. package/dist/controller/kanban-board.d.ts +2 -3
  40. package/dist/controller/kanban-board.js +43 -24
  41. package/dist/controller/list-controller.d.ts +5 -5
  42. package/dist/controller/list-controller.js +444 -400
  43. package/dist/controller/list-params.js +96 -87
  44. package/dist/controller/pivot-grid.js +89 -95
  45. package/dist/controller/prev-next.d.ts +2 -2
  46. package/dist/controller/prev-next.js +74 -54
  47. package/dist/controller/record-controllers.d.ts +14 -6
  48. package/dist/controller/record-controllers.js +288 -257
  49. package/dist/controller/record-diff.d.ts +21 -0
  50. package/dist/controller/record-diff.js +50 -0
  51. package/dist/controller/record-timeline.js +42 -30
  52. package/dist/controller/record-tree.js +57 -47
  53. package/dist/controller/record-workflow.d.ts +38 -16
  54. package/dist/controller/record-workflow.js +85 -64
  55. package/dist/controller/reference-many-controller.js +198 -192
  56. package/dist/controller/reference-one-controller.d.ts +1 -1
  57. package/dist/controller/reference-one-controller.js +63 -56
  58. package/dist/controller/saved-queries.js +104 -91
  59. package/dist/controller/translate-fallback.js +10 -6
  60. package/dist/csv/export.d.ts +2 -1
  61. package/dist/csv/export.js +43 -40
  62. package/dist/csv/import-controller.js +185 -169
  63. package/dist/csv/match-headers.js +29 -22
  64. package/dist/csv/parse-with.js +60 -54
  65. package/dist/csv/run-import.js +69 -72
  66. package/dist/csv/validate-rows.d.ts +1 -1
  67. package/dist/csv/validate-rows.js +65 -52
  68. package/dist/data/cache-ops.d.ts +5 -4
  69. package/dist/data/cache-ops.js +208 -188
  70. package/dist/data/combine-data-providers.js +25 -23
  71. package/dist/data/data-provider-log.d.ts +45 -0
  72. package/dist/data/data-provider-log.js +93 -0
  73. package/dist/data/errors.js +14 -10
  74. package/dist/data/fake-provider.d.ts +6 -3
  75. package/dist/data/fake-provider.js +129 -114
  76. package/dist/data/fetch-utils.js +61 -44
  77. package/dist/data/filter-branch.d.ts +5 -0
  78. package/dist/data/filter-branch.js +10 -0
  79. package/dist/data/get-many-aggregate.js +84 -76
  80. package/dist/data/http-error.js +17 -12
  81. package/dist/data/keys.js +50 -36
  82. package/dist/data/mutation-options.d.ts +14 -9
  83. package/dist/data/mutation-options.js +93 -109
  84. package/dist/data/query-options.js +73 -68
  85. package/dist/data/remove-empty.d.ts +3 -0
  86. package/dist/data/remove-empty.js +11 -0
  87. package/dist/data/undoable.d.ts +2 -2
  88. package/dist/data/undoable.js +190 -174
  89. package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
  90. package/dist/data/with-lifecycle-callbacks.js +66 -52
  91. package/dist/environment/application-update-store.js +58 -55
  92. package/dist/environment/offline-store.js +21 -21
  93. package/dist/form/array-binding.d.ts +30 -11
  94. package/dist/form/array-binding.js +184 -166
  95. package/dist/form/choice-ids.d.ts +7 -0
  96. package/dist/form/choice-ids.js +11 -0
  97. package/dist/form/choices-controller.d.ts +3 -2
  98. package/dist/form/choices-controller.js +302 -275
  99. package/dist/form/date-format.d.ts +38 -0
  100. package/dist/form/date-format.js +89 -0
  101. package/dist/form/dirty-values.d.ts +2 -2
  102. package/dist/form/dirty-values.js +22 -11
  103. package/dist/form/duration.d.ts +22 -0
  104. package/dist/form/duration.js +49 -0
  105. package/dist/form/edit-create-form.d.ts +12 -5
  106. package/dist/form/edit-create-form.js +243 -188
  107. package/dist/form/event-value.d.ts +6 -0
  108. package/dist/form/event-value.js +17 -0
  109. package/dist/form/field-error.d.ts +14 -0
  110. package/dist/form/field-error.js +28 -0
  111. package/dist/form/field-name.d.ts +20 -0
  112. package/dist/form/field-name.js +6 -0
  113. package/dist/form/filter-form-values.js +63 -49
  114. package/dist/form/form-controller.d.ts +9 -1
  115. package/dist/form/form-controller.js +165 -160
  116. package/dist/form/form-groups.d.ts +4 -3
  117. package/dist/form/form-groups.js +79 -79
  118. package/dist/form/form-handle.d.ts +175 -0
  119. package/dist/form/form-handle.js +68 -0
  120. package/dist/form/input-binding.d.ts +56 -31
  121. package/dist/form/input-binding.js +169 -118
  122. package/dist/form/input-values.d.ts +130 -0
  123. package/dist/form/input-values.js +177 -0
  124. package/dist/form/json-text.js +51 -37
  125. package/dist/form/save-pipeline.d.ts +6 -8
  126. package/dist/form/save-pipeline.js +91 -82
  127. package/dist/form/set-form-values.d.ts +6 -0
  128. package/dist/form/set-form-values.js +10 -0
  129. package/dist/form/source.js +97 -65
  130. package/dist/form/text-of.d.ts +2 -0
  131. package/dist/form/text-of.js +7 -0
  132. package/dist/form/validators.d.ts +58 -19
  133. package/dist/form/validators.js +97 -71
  134. package/dist/geo/bbox.d.ts +5 -4
  135. package/dist/geo/bbox.js +69 -48
  136. package/dist/geo/bounds.d.ts +14 -3
  137. package/dist/geo/bounds.js +98 -68
  138. package/dist/geo/constraints.js +88 -93
  139. package/dist/geo/coordinates.js +31 -23
  140. package/dist/geo/editor.d.ts +13 -13
  141. package/dist/geo/editor.js +176 -153
  142. package/dist/geo/geocoding.d.ts +4 -4
  143. package/dist/geo/geocoding.js +38 -41
  144. package/dist/geo/geojson.d.ts +12 -74
  145. package/dist/geo/geojson.js +52 -40
  146. package/dist/geo/geometry-math.d.ts +4 -4
  147. package/dist/geo/geometry-math.js +100 -93
  148. package/dist/geo/index.d.ts +7 -6
  149. package/dist/geo/map-layers.d.ts +42 -7
  150. package/dist/geo/map-layers.js +88 -62
  151. package/dist/geo/map-style.js +31 -32
  152. package/dist/geo/messages.js +48 -45
  153. package/dist/geo/nominatim.js +88 -80
  154. package/dist/geo/osm-presets.d.ts +4 -3
  155. package/dist/geo/osm-presets.js +108 -86
  156. package/dist/geo/osm-tags.js +14 -12
  157. package/dist/geo/overpass.d.ts +14 -2
  158. package/dist/geo/overpass.js +83 -90
  159. package/dist/geo/shape-value.d.ts +9 -5
  160. package/dist/geo/shape-value.js +169 -146
  161. package/dist/geo/snap-to-roads.d.ts +3 -3
  162. package/dist/geo/snap-to-roads.js +55 -50
  163. package/dist/geo/throttle.js +36 -39
  164. package/dist/guards.d.ts +37 -0
  165. package/dist/guards.js +62 -0
  166. package/dist/i18n/default-messages.js +34 -29
  167. package/dist/i18n/english-messages.js +187 -189
  168. package/dist/i18n/i18n.d.ts +2 -2
  169. package/dist/i18n/i18n.js +47 -60
  170. package/dist/i18n/inflection.d.ts +34 -0
  171. package/dist/i18n/inflection.js +82 -0
  172. package/dist/i18n/messages.d.ts +10 -2
  173. package/dist/i18n/messages.js +17 -4
  174. package/dist/i18n/missing-keys.js +66 -62
  175. package/dist/i18n/translatable.d.ts +16 -1
  176. package/dist/i18n/translatable.js +40 -47
  177. package/dist/index.d.ts +71 -24
  178. package/dist/index.js +40 -288
  179. package/dist/json/deep-equal.d.ts +8 -0
  180. package/dist/json/deep-equal.js +24 -0
  181. package/dist/json/strip-json-comments.js +38 -42
  182. package/dist/noop.d.ts +2 -0
  183. package/dist/noop.js +4 -0
  184. package/dist/notification/error-message.d.ts +2 -0
  185. package/dist/notification/error-message.js +10 -0
  186. package/dist/notification/notification-store.d.ts +3 -2
  187. package/dist/notification/notification-store.js +41 -45
  188. package/dist/onboarding-tour/placement.js +63 -78
  189. package/dist/onboarding-tour/tour-controller.js +46 -54
  190. package/dist/realtime/add-events-for-mutations.js +75 -81
  191. package/dist/realtime/announce-lock.js +16 -13
  192. package/dist/realtime/broadcast-channel-transport.js +65 -59
  193. package/dist/realtime/copy-members.js +22 -14
  194. package/dist/realtime/in-memory-lock-provider.js +40 -48
  195. package/dist/realtime/live.js +95 -101
  196. package/dist/realtime/lock-options.js +16 -16
  197. package/dist/realtime/lock-session.js +91 -78
  198. package/dist/realtime/memory-transport.js +25 -25
  199. package/dist/realtime/presence.js +120 -122
  200. package/dist/realtime/realtime-data-provider.d.ts +10 -0
  201. package/dist/realtime/realtime-data-provider.js +45 -42
  202. package/dist/realtime/reconnect-backoff.js +17 -0
  203. package/dist/realtime/sse-transport.d.ts +31 -0
  204. package/dist/realtime/sse-transport.js +235 -0
  205. package/dist/realtime/status-store.js +23 -28
  206. package/dist/realtime/topic-hub.js +62 -71
  207. package/dist/realtime/topics.js +10 -12
  208. package/dist/realtime/types.js +10 -12
  209. package/dist/realtime/unread-count.js +21 -21
  210. package/dist/realtime/websocket-transport.d.ts +32 -0
  211. package/dist/realtime/websocket-transport.js +306 -0
  212. package/dist/resource/describe-resource.d.ts +2 -0
  213. package/dist/resource/describe-resource.js +232 -219
  214. package/dist/resource/get-at-path.js +29 -14
  215. package/dist/resource/identifier.d.ts +18 -0
  216. package/dist/resource/identifier.js +27 -0
  217. package/dist/resource/record.d.ts +4 -0
  218. package/dist/resource/record.js +10 -0
  219. package/dist/resource/register.d.ts +7 -0
  220. package/dist/resource/registry-store.d.ts +45 -15
  221. package/dist/resource/registry-store.js +67 -60
  222. package/dist/resource/registry.d.ts +10 -0
  223. package/dist/resource/registry.js +8 -4
  224. package/dist/resource/source-path.d.ts +16 -1
  225. package/dist/rich-text/rich-text-extras.d.ts +132 -0
  226. package/dist/rich-text/rich-text-extras.js +179 -0
  227. package/dist/rich-text/rich-text-image.d.ts +124 -0
  228. package/dist/rich-text/rich-text-image.js +163 -0
  229. package/dist/rich-text/rich-text.d.ts +6 -0
  230. package/dist/rich-text/rich-text.js +125 -88
  231. package/dist/router/location.d.ts +22 -4
  232. package/dist/router/location.js +41 -18
  233. package/dist/router/match.js +47 -39
  234. package/dist/router/paths.js +40 -34
  235. package/dist/router/record-from-location.d.ts +1 -1
  236. package/dist/router/record-from-location.js +25 -17
  237. package/dist/store/adapters.js +51 -55
  238. package/dist/store/micro-store.js +57 -59
  239. package/dist/store/shallow-equal.d.ts +12 -0
  240. package/dist/store/shallow-equal.js +20 -0
  241. package/dist/store/store.js +54 -57
  242. package/package.json +13 -11
@@ -2,6 +2,7 @@ import { type DataContext } from "../data/query-options.js";
2
2
  import type { FilterPayload, PageInfo, SortPayload } from "../data/types.js";
3
3
  import type { NotificationStore } from "../notification/notification-store.js";
4
4
  import type { RecordOf, ResourceMap } from "../resource/registry.js";
5
+ import type { SourceKey } from "../resource/source-path.js";
5
6
  import { type MicroStore } from "../store/micro-store.js";
6
7
  /**
7
8
  * The choices-context state (§0b parity). A list-like base (page/perPage/sort/
@@ -31,7 +32,7 @@ export interface ChoicesControllerState<TRecord> {
31
32
  hasPreviousPage: boolean | undefined;
32
33
  isPending: boolean;
33
34
  isFetching: boolean;
34
- error: unknown;
35
+ error: Error | null;
35
36
  /** The referenced resource name. */
36
37
  resource: string;
37
38
  /** The FK field on the referencing side (metadata for consumers). */
@@ -62,7 +63,7 @@ export interface ChoicesControllerOptions<TResources extends ResourceMap = Resou
62
63
  * `'id'` (default) resolves selected records via `getMany`; any other key
63
64
  * resolves via `getList({ filter: { [lookupKey]: value } })` (#16).
64
65
  */
65
- lookupKey?: string;
66
+ lookupKey?: SourceKey<RecordOf<TResources, TName>>;
66
67
  /** Permanent filter — merged OVER the dynamic filter for every choices fetch. */
67
68
  filter?: FilterPayload;
68
69
  page?: number;
@@ -1,281 +1,308 @@
1
- import { QueryObserver, keepPreviousData } from "@tanstack/query-core";
1
+ import { notifyFetchError } from "../controller/list-controller.js";
2
2
  import { getListOptions, getManyOptions } from "../data/query-options.js";
3
+ import { removeEmpty } from "../data/remove-empty.js";
3
4
  import { createMicroStore } from "../store/micro-store.js";
4
- const DEFAULT_PAGE = 1;
5
- const DEFAULT_PER_PAGE = 25;
6
- const DEFAULT_DEBOUNCE_MS = 500;
7
- const DEFAULT_SORT = { field: "id", order: "DESC" };
8
- const SELECTED_LIST_PER_PAGE = 1e3;
9
- function removeEmpty(filter) {
10
- const result = {};
11
- for (const [key, value] of Object.entries(filter)) {
12
- if (value === "" || value === null || value === void 0) continue;
13
- result[key] = value;
14
- }
15
- return result;
16
- }
17
- function errorMessageKey(error) {
18
- const message = error instanceof Error ? error.message : "";
19
- return message || "chelone.notification.http_error";
20
- }
5
+ import { QueryObserver, keepPreviousData } from "@tanstack/query-core";
6
+ // src/form/choices-controller.ts
7
+ var DEFAULT_PAGE = 1;
8
+ var DEFAULT_PER_PAGE = 25;
9
+ var DEFAULT_DEBOUNCE_MS = 500;
10
+ var DEFAULT_SORT = {
11
+ field: "id",
12
+ order: "DESC"
13
+ };
14
+ /** perPage for the non-id selected getList — generous, real values sets are small. */
15
+ var SELECTED_LIST_PER_PAGE = 1e3;
16
+ /** Normalizes a value into a non-empty identifier list, or `[]` when there is nothing to resolve. */
21
17
  function toValueList(value) {
22
- const raw = Array.isArray(value) ? value : [value];
23
- return raw.filter(
24
- (v) => v != null && v !== "" && (typeof v === "string" || typeof v === "number")
25
- );
18
+ return (Array.isArray(value) ? value : [value]).filter((v) => v != null && v !== "" && (typeof v === "string" || typeof v === "number"));
26
19
  }
20
+ /**
21
+ * Vanilla choices controller: TWO QueryObservers (choices page + selected
22
+ * resolution) mapped into one micro-store, closing charter §11 #16 — selected
23
+ * records resolve by an arbitrary `lookupKey`, not just `id`.
24
+ *
25
+ * Lifecycle (P4 4R): construction is INERT — pure state + closures, zero
26
+ * subscriptions, zero fetching. Acquisition (both observers + KV nothing) runs
27
+ * on the micro-store's FIRST subscriber; the LAST unsubscribe releases both
28
+ * observers and clears the debounce timer. Attach/detach cycles are
29
+ * re-entrant; re-attach rebuilds both observers from CURRENT state.
30
+ * `destroy()` remains the hard imperative teardown for non-React consumers.
31
+ *
32
+ * Loop-prevention: `observer.setOptions` is called ONLY from param-mutating
33
+ * actions (`setPage`/`setSort`/`setFilters`/`setValue`), never from the
34
+ * observer result subscription — the subscription only maps results into state.
35
+ */
27
36
  function createChoicesController(ctx, options) {
28
- const reference = options.reference;
29
- const lookupKey = options.lookupKey ?? "id";
30
- const isIdLookup = lookupKey === "id";
31
- const permanentFilter = options.filter ?? {};
32
- const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
33
- const store = createMicroStore(
34
- {
35
- page: options.page ?? DEFAULT_PAGE,
36
- perPage: options.perPage ?? DEFAULT_PER_PAGE,
37
- sort: { ...options.sort ?? DEFAULT_SORT },
38
- filter: {},
39
- allChoices: [],
40
- availableChoices: [],
41
- selectedChoices: [],
42
- total: void 0,
43
- pageInfo: void 0,
44
- hasNextPage: void 0,
45
- hasPreviousPage: void 0,
46
- isPending: true,
47
- isFetching: false,
48
- error: null,
49
- resource: reference,
50
- source: options.source,
51
- isFromReference: true
52
- },
53
- { onSubscribed: acquire }
54
- );
55
- let destroyed = false;
56
- store.onDestroy(() => {
57
- destroyed = true;
58
- });
59
- let choicesObserver = null;
60
- let selectedObserver = null;
61
- let selectedValues = [];
62
- let pageData;
63
- let pageTotal;
64
- let pageInfo;
65
- let selectedData = [];
66
- const isGetChoicesEnabled = () => options.enableGetChoices ? options.enableGetChoices(store.getSnapshot().filter) : true;
67
- const buildChoicesOptions = () => {
68
- const s = store.getSnapshot();
69
- return {
70
- ...getListOptions(ctx, reference, {
71
- pagination: { page: s.page, perPage: s.perPage },
72
- sort: s.sort,
73
- filter: { ...s.filter, ...permanentFilter },
74
- meta: options.meta
75
- }),
76
- placeholderData: keepPreviousData,
77
- retry: false,
78
- enabled: isGetChoicesEnabled()
79
- };
80
- };
81
- const buildSelectedOptions = () => {
82
- const enabled = selectedValues.length > 0;
83
- if (isIdLookup) {
84
- return {
85
- ...getManyOptions(ctx, reference, {
86
- ids: selectedValues,
87
- meta: options.meta
88
- }),
89
- retry: false,
90
- enabled
91
- };
92
- }
93
- const filterValue = selectedValues.length === 1 ? selectedValues[0] : selectedValues;
94
- return {
95
- ...getListOptions(ctx, reference, {
96
- pagination: { page: 1, perPage: SELECTED_LIST_PER_PAGE },
97
- filter: { [lookupKey]: filterValue },
98
- meta: options.meta
99
- }),
100
- retry: false,
101
- enabled
102
- };
103
- };
104
- const readSelectedData = (result) => {
105
- const data = result.data;
106
- if (data == null) return [];
107
- return Array.isArray(data) ? data : data.data;
108
- };
109
- const recompute = (patch) => {
110
- const s = store.getSnapshot();
111
- const page = pageData ?? [];
112
- const selected = selectedData;
113
- const keyOf = (record) => record[lookupKey];
114
- const pageKeys = new Set(page.map(keyOf));
115
- const missing = selected.filter((record) => !pageKeys.has(keyOf(record)));
116
- const allChoices = missing.length > 0 ? [...missing, ...page] : page;
117
- let total = pageTotal;
118
- if (total !== void 0 && missing.length === 1 && selected.length === 1) {
119
- total = total + 1;
120
- }
121
- let hasNextPage;
122
- let hasPreviousPage;
123
- if (pageInfo) {
124
- hasNextPage = pageInfo.hasNextPage;
125
- hasPreviousPage = pageInfo.hasPreviousPage;
126
- } else if (pageTotal !== void 0) {
127
- hasNextPage = s.page * s.perPage < pageTotal;
128
- hasPreviousPage = s.page > 1;
129
- }
130
- store.setState({
131
- allChoices,
132
- availableChoices: page,
133
- selectedChoices: selected,
134
- total,
135
- pageInfo,
136
- hasNextPage,
137
- hasPreviousPage,
138
- isPending: patch.isPending,
139
- isFetching: patch.isFetching,
140
- error: patch.error
141
- });
142
- };
143
- let lastNotifiedError = null;
144
- const notifyError = (error) => {
145
- if (error === lastNotifiedError) return;
146
- lastNotifiedError = error;
147
- if (error == null) return;
148
- ctx.notifications?.notify(errorMessageKey(error), {
149
- type: "error",
150
- messageArgs: { _: String(error) }
151
- });
152
- };
153
- const onChoicesResult = (result) => {
154
- const list = result.data;
155
- pageData = list?.data;
156
- pageTotal = list?.total;
157
- pageInfo = list?.pageInfo;
158
- const selectedPending = selectedValues.length > 0 && selectedObserver ? selectedObserver.getCurrentResult().isPending : false;
159
- const selectedFetching = selectedValues.length > 0 && selectedObserver ? selectedObserver.getCurrentResult().isFetching : false;
160
- const error = result.error ?? selectedObserver?.getCurrentResult().error ?? null;
161
- recompute({
162
- isPending: result.isPending || selectedPending,
163
- isFetching: result.isFetching || selectedFetching,
164
- error
165
- });
166
- notifyError(result.error ?? null);
167
- };
168
- const onSelectedResult = (result) => {
169
- selectedData = readSelectedData(result);
170
- const choicesResult = choicesObserver?.getCurrentResult();
171
- const choicesPending = choicesResult?.isPending ?? false;
172
- const choicesFetching = choicesResult?.isFetching ?? false;
173
- const selectedPending = selectedValues.length > 0 && result.isPending;
174
- const selectedFetching = selectedValues.length > 0 && result.isFetching;
175
- const error = choicesResult?.error ?? result.error ?? null;
176
- recompute({
177
- isPending: choicesPending || selectedPending,
178
- isFetching: choicesFetching || selectedFetching,
179
- error
180
- });
181
- };
182
- const setPage = (page) => {
183
- if (destroyed) return;
184
- store.setState({ page });
185
- choicesObserver?.setOptions(buildChoicesOptions());
186
- };
187
- const setPerPage = (perPage) => {
188
- if (destroyed) return;
189
- store.setState({ perPage, page: 1 });
190
- choicesObserver?.setOptions(buildChoicesOptions());
191
- };
192
- const setSort = (sort) => {
193
- if (destroyed) return;
194
- store.setState({ sort: { ...sort }, page: 1 });
195
- choicesObserver?.setOptions(buildChoicesOptions());
196
- };
197
- const applyFilters = (filter) => {
198
- store.setState({ filter: removeEmpty(filter), page: 1 });
199
- choicesObserver?.setOptions(buildChoicesOptions());
200
- };
201
- let debounceTimer;
202
- const clearDebounce = () => {
203
- if (debounceTimer !== void 0) {
204
- clearTimeout(debounceTimer);
205
- debounceTimer = void 0;
206
- }
207
- };
208
- store.onDestroy(clearDebounce);
209
- const setFilters = (filter, debounce = false) => {
210
- if (destroyed) return;
211
- clearDebounce();
212
- if (!debounce) {
213
- applyFilters(filter);
214
- return;
215
- }
216
- debounceTimer = setTimeout(() => {
217
- debounceTimer = void 0;
218
- if (destroyed) return;
219
- applyFilters(filter);
220
- }, debounceMs);
221
- };
222
- const setValue = (value) => {
223
- if (destroyed) return;
224
- const next = toValueList(value);
225
- if (next.length === selectedValues.length && next.every((v, i) => v === selectedValues[i])) {
226
- return;
227
- }
228
- selectedValues = next;
229
- if (next.length === 0) {
230
- selectedData = [];
231
- }
232
- selectedObserver?.setOptions(buildSelectedOptions());
233
- if (!selectedObserver && next.length === 0) {
234
- recompute({
235
- isPending: store.getSnapshot().isPending,
236
- isFetching: store.getSnapshot().isFetching,
237
- error: store.getSnapshot().error
238
- });
239
- }
240
- };
241
- const refetch = () => {
242
- if (destroyed) return;
243
- void choicesObserver?.refetch();
244
- if (selectedValues.length > 0) void selectedObserver?.refetch();
245
- };
246
- function acquire() {
247
- const choices = new QueryObserver(
248
- ctx.queryClient,
249
- buildChoicesOptions()
250
- );
251
- choicesObserver = choices;
252
- const selected = new QueryObserver(
253
- ctx.queryClient,
254
- buildSelectedOptions()
255
- );
256
- selectedObserver = selected;
257
- const unsubChoices = choices.subscribe(onChoicesResult);
258
- const unsubSelected = selected.subscribe(onSelectedResult);
259
- onSelectedResult(selected.getCurrentResult());
260
- onChoicesResult(choices.getCurrentResult());
261
- return () => {
262
- unsubChoices();
263
- unsubSelected();
264
- choicesObserver = null;
265
- selectedObserver = null;
266
- clearDebounce();
267
- };
268
- }
269
- return {
270
- ...store,
271
- setPage,
272
- setPerPage,
273
- setSort,
274
- setFilters,
275
- setValue,
276
- refetch
277
- };
37
+ const reference = options.reference;
38
+ const lookupKey = options.lookupKey ?? "id";
39
+ const isIdLookup = lookupKey === "id";
40
+ const permanentFilter = options.filter ?? {};
41
+ const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
42
+ const store = createMicroStore({
43
+ page: options.page ?? DEFAULT_PAGE,
44
+ perPage: options.perPage ?? DEFAULT_PER_PAGE,
45
+ sort: { ...options.sort ?? DEFAULT_SORT },
46
+ filter: {},
47
+ allChoices: [],
48
+ availableChoices: [],
49
+ selectedChoices: [],
50
+ total: void 0,
51
+ pageInfo: void 0,
52
+ hasNextPage: void 0,
53
+ hasPreviousPage: void 0,
54
+ isPending: true,
55
+ isFetching: false,
56
+ error: null,
57
+ resource: reference,
58
+ source: options.source,
59
+ isFromReference: true
60
+ }, { onSubscribed: acquire });
61
+ let destroyed = false;
62
+ store.onDestroy(() => {
63
+ destroyed = true;
64
+ });
65
+ /** Non-null exactly while attached. */
66
+ let choicesObserver = null;
67
+ /** Selected observer: getMany (id) OR getList (non-id). Rebuilt on setValue. */
68
+ let selectedObserver = null;
69
+ /** The value(s) currently driving selected resolution. */
70
+ let selectedValues = [];
71
+ /** Latest raw fetch results, cached so `recompute()` can re-merge on any change. */
72
+ let pageData;
73
+ let pageTotal;
74
+ let pageInfo;
75
+ let selectedData = [];
76
+ const isGetChoicesEnabled = () => options.enableGetChoices ? options.enableGetChoices(store.getSnapshot().filter) : true;
77
+ const buildChoicesOptions = () => {
78
+ const s = store.getSnapshot();
79
+ return {
80
+ ...getListOptions(ctx, reference, {
81
+ pagination: {
82
+ page: s.page,
83
+ perPage: s.perPage
84
+ },
85
+ sort: s.sort,
86
+ filter: {
87
+ ...s.filter,
88
+ ...permanentFilter
89
+ },
90
+ meta: options.meta
91
+ }),
92
+ placeholderData: keepPreviousData,
93
+ retry: false,
94
+ enabled: isGetChoicesEnabled()
95
+ };
96
+ };
97
+ /**
98
+ * Options for the selected observer. `id` lookup batches through getMany;
99
+ * any other key resolves through a getList filter (single OR array value).
100
+ * Disabled when there is nothing to resolve so it never pins `isPending`.
101
+ */
102
+ const buildSelectedOptions = () => {
103
+ const enabled = selectedValues.length > 0;
104
+ if (isIdLookup) return {
105
+ ...getManyOptions(ctx, reference, {
106
+ ids: selectedValues,
107
+ meta: options.meta
108
+ }),
109
+ retry: false,
110
+ enabled
111
+ };
112
+ const filterValue = selectedValues.length === 1 ? selectedValues[0] : selectedValues;
113
+ return {
114
+ ...getListOptions(ctx, reference, {
115
+ pagination: {
116
+ page: 1,
117
+ perPage: SELECTED_LIST_PER_PAGE
118
+ },
119
+ filter: { [lookupKey]: filterValue },
120
+ meta: options.meta
121
+ }),
122
+ retry: false,
123
+ enabled
124
+ };
125
+ };
126
+ /** Records from the selected observer result, whichever shape it is. */
127
+ const readSelectedData = (result) => {
128
+ const data = result.data;
129
+ if (data == null) return [];
130
+ return Array.isArray(data) ? data : data.data;
131
+ };
132
+ /**
133
+ * Merge the page + selected records into the three views (§0b parity), then
134
+ * write the derived state. Called from BOTH result subscriptions.
135
+ */
136
+ const recompute = (patch) => {
137
+ const s = store.getSnapshot();
138
+ const page = pageData ?? [];
139
+ const selected = selectedData;
140
+ const keyOf = (record) => record[lookupKey];
141
+ const pageKeys = new Set(page.map(keyOf));
142
+ const missing = selected.filter((record) => !pageKeys.has(keyOf(record)));
143
+ const allChoices = missing.length > 0 ? [...missing, ...page] : page;
144
+ let total = pageTotal;
145
+ if (total !== void 0 && missing.length === 1 && selected.length === 1) total = total + 1;
146
+ let hasNextPage;
147
+ let hasPreviousPage;
148
+ if (pageInfo) {
149
+ hasNextPage = pageInfo.hasNextPage;
150
+ hasPreviousPage = pageInfo.hasPreviousPage;
151
+ } else if (pageTotal !== void 0) {
152
+ hasNextPage = s.page * s.perPage < pageTotal;
153
+ hasPreviousPage = s.page > 1;
154
+ }
155
+ store.setState({
156
+ allChoices,
157
+ availableChoices: page,
158
+ selectedChoices: selected,
159
+ total,
160
+ pageInfo,
161
+ hasNextPage,
162
+ hasPreviousPage,
163
+ isPending: patch.isPending,
164
+ isFetching: patch.isFetching,
165
+ error: patch.error
166
+ });
167
+ };
168
+ let lastNotifiedError = null;
169
+ const notifyError = (error) => {
170
+ if (error === lastNotifiedError) return;
171
+ lastNotifiedError = error;
172
+ if (error === null) return;
173
+ notifyFetchError(ctx.notifications, error);
174
+ };
175
+ /**
176
+ * The combined status union. `isPending` reflects the choices query and the
177
+ * selected query ONLY when a value is being resolved — a valueless selected
178
+ * query is disabled and must never pin `isPending` (§0b).
179
+ */
180
+ const onChoicesResult = (result) => {
181
+ const list = result.data;
182
+ pageData = list?.data;
183
+ pageTotal = list?.total;
184
+ pageInfo = list?.pageInfo;
185
+ const selectedPending = selectedValues.length > 0 && selectedObserver ? selectedObserver.getCurrentResult().isPending : false;
186
+ const selectedFetching = selectedValues.length > 0 && selectedObserver ? selectedObserver.getCurrentResult().isFetching : false;
187
+ const error = result.error ?? selectedObserver?.getCurrentResult().error ?? null;
188
+ recompute({
189
+ isPending: result.isPending || selectedPending,
190
+ isFetching: result.isFetching || selectedFetching,
191
+ error
192
+ });
193
+ notifyError(result.error ?? null);
194
+ };
195
+ const onSelectedResult = (result) => {
196
+ selectedData = readSelectedData(result);
197
+ const choicesResult = choicesObserver?.getCurrentResult();
198
+ const choicesPending = choicesResult?.isPending ?? false;
199
+ const choicesFetching = choicesResult?.isFetching ?? false;
200
+ const selectedPending = selectedValues.length > 0 && result.isPending;
201
+ const selectedFetching = selectedValues.length > 0 && result.isFetching;
202
+ const error = choicesResult?.error ?? result.error ?? null;
203
+ recompute({
204
+ isPending: choicesPending || selectedPending,
205
+ isFetching: choicesFetching || selectedFetching,
206
+ error
207
+ });
208
+ };
209
+ const setPage = (page) => {
210
+ if (destroyed) return;
211
+ store.setState({ page });
212
+ choicesObserver?.setOptions(buildChoicesOptions());
213
+ };
214
+ const setPerPage = (perPage) => {
215
+ if (destroyed) return;
216
+ store.setState({
217
+ perPage,
218
+ page: 1
219
+ });
220
+ choicesObserver?.setOptions(buildChoicesOptions());
221
+ };
222
+ const setSort = (sort) => {
223
+ if (destroyed) return;
224
+ store.setState({
225
+ sort: { ...sort },
226
+ page: 1
227
+ });
228
+ choicesObserver?.setOptions(buildChoicesOptions());
229
+ };
230
+ const applyFilters = (filter) => {
231
+ store.setState({
232
+ filter: removeEmpty(filter),
233
+ page: 1
234
+ });
235
+ choicesObserver?.setOptions(buildChoicesOptions());
236
+ };
237
+ let debounceTimer;
238
+ const clearDebounce = () => {
239
+ if (debounceTimer !== void 0) {
240
+ clearTimeout(debounceTimer);
241
+ debounceTimer = void 0;
242
+ }
243
+ };
244
+ store.onDestroy(clearDebounce);
245
+ const setFilters = (filter, debounce = false) => {
246
+ if (destroyed) return;
247
+ clearDebounce();
248
+ if (!debounce) {
249
+ applyFilters(filter);
250
+ return;
251
+ }
252
+ debounceTimer = setTimeout(() => {
253
+ debounceTimer = void 0;
254
+ if (destroyed) return;
255
+ applyFilters(filter);
256
+ }, debounceMs);
257
+ };
258
+ const setValue = (value) => {
259
+ if (destroyed) return;
260
+ const next = toValueList(value);
261
+ if (next.length === selectedValues.length && next.every((v, i) => v === selectedValues[i])) return;
262
+ selectedValues = next;
263
+ if (next.length === 0) selectedData = [];
264
+ selectedObserver?.setOptions(buildSelectedOptions());
265
+ if (!selectedObserver && next.length === 0) recompute({
266
+ isPending: store.getSnapshot().isPending,
267
+ isFetching: store.getSnapshot().isFetching,
268
+ error: store.getSnapshot().error
269
+ });
270
+ };
271
+ const refetch = () => {
272
+ if (destroyed) return;
273
+ choicesObserver?.refetch();
274
+ if (selectedValues.length > 0) selectedObserver?.refetch();
275
+ };
276
+ /**
277
+ * Attach: runs on the micro-store's FIRST subscriber. Creates and subscribes
278
+ * BOTH observers with options built from CURRENT state, and returns the
279
+ * teardown the micro-store runs on last-unsubscribe AND destroy-while-attached.
280
+ */
281
+ function acquire() {
282
+ const choices = new QueryObserver(ctx.queryClient, buildChoicesOptions());
283
+ choicesObserver = choices;
284
+ const selected = new QueryObserver(ctx.queryClient, buildSelectedOptions());
285
+ selectedObserver = selected;
286
+ const unsubChoices = choices.subscribe(onChoicesResult);
287
+ const unsubSelected = selected.subscribe(onSelectedResult);
288
+ onSelectedResult(selected.getCurrentResult());
289
+ onChoicesResult(choices.getCurrentResult());
290
+ return () => {
291
+ unsubChoices();
292
+ unsubSelected();
293
+ choicesObserver = null;
294
+ selectedObserver = null;
295
+ clearDebounce();
296
+ };
297
+ }
298
+ return {
299
+ ...store,
300
+ setPage,
301
+ setPerPage,
302
+ setSort,
303
+ setFilters,
304
+ setValue,
305
+ refetch
306
+ };
278
307
  }
279
- export {
280
- createChoicesController
281
- };
308
+ export { createChoicesController };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pure date, date-time and time format and parse helpers shared by the React and
3
+ * Vue `<DateInput>`, `<DateTimeInput>` and `<TimeInput>`, so both adapters
4
+ * format and validate a value the same way.
5
+ */
6
+ import type { DateInputValue } from "./input-values.js";
7
+ /**
8
+ * Format a form value (string / ISO / `Date` / timestamp / "") to a bare
9
+ * "YYYY-MM-DD" string for `<input type="date">`. Returns `null` for nullish/empty.
10
+ */
11
+ export declare function formatDate(value: DateInputValue): string | null;
12
+ /** Whether a raw `<input type="date">` string is empty or a valid date. */
13
+ export declare function isValidDate(raw: string): boolean;
14
+ /**
15
+ * Format a form value (ISO string / `Date` / timestamp) to the local
16
+ * `datetime-local` display string. Empty → "" (keeps the input controlled).
17
+ */
18
+ export declare function formatDateTime(value: DateInputValue): string;
19
+ /**
20
+ * Parse a local `datetime-local` string back to an ISO 8601 string (browser
21
+ * timezone). Returns `null` for empty/unparseable so a required validator fires.
22
+ */
23
+ export declare function parseDateTimeToISO(value: string): string | null;
24
+ /** Whether a raw `datetime-local` string is empty or parses to a valid date. */
25
+ export declare function isValidDateTime(raw: string): boolean;
26
+ /**
27
+ * Format a form value (`Date` / "HH:mm" string) to the "HH:mm" display string.
28
+ * Empty → "" (keeps the input controlled).
29
+ */
30
+ export declare function formatTime(value: DateInputValue): string;
31
+ /**
32
+ * Parse a "HH:mm"/"HH:mm:ss" string to a `Date` (today at that local time), for
33
+ * dataProvider compatibility. Returns `null` for empty/unparseable so a required
34
+ * validator still fires.
35
+ */
36
+ export declare function parseTimeToDate(value: string): Date | null;
37
+ /** Whether a raw `<input type="time">` string is empty or a valid time. */
38
+ export declare function isValidTime(raw: string): boolean;