@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
@@ -2,7 +2,14 @@ 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";
7
+ /**
8
+ * An erased choice record: choices carry arbitrary shapes (`{id, name, ...}`),
9
+ * and an adapter's choice inputs are generic over one. Both adapters re-export
10
+ * this as their own `ChoiceRecord`.
11
+ */
12
+ export type ChoiceRecord = Record<string, unknown>;
6
13
  /**
7
14
  * The choices-context state (§0b parity). A list-like base (page/perPage/sort/
8
15
  * filter) plus a status union over three record views:
@@ -31,7 +38,7 @@ export interface ChoicesControllerState<TRecord> {
31
38
  hasPreviousPage: boolean | undefined;
32
39
  isPending: boolean;
33
40
  isFetching: boolean;
34
- error: unknown;
41
+ error: Error | null;
35
42
  /** The referenced resource name. */
36
43
  resource: string;
37
44
  /** The FK field on the referencing side (metadata for consumers). */
@@ -62,7 +69,7 @@ export interface ChoicesControllerOptions<TResources extends ResourceMap = Resou
62
69
  * `'id'` (default) resolves selected records via `getMany`; any other key
63
70
  * resolves via `getList({ filter: { [lookupKey]: value } })` (#16).
64
71
  */
65
- lookupKey?: string;
72
+ lookupKey?: SourceKey<RecordOf<TResources, TName>>;
66
73
  /** Permanent filter — merged OVER the dynamic filter for every choices fetch. */
67
74
  filter?: FilterPayload;
68
75
  page?: number;
@@ -93,3 +100,16 @@ export interface ChoicesControllerOptions<TResources extends ResourceMap = Resou
93
100
  * observer result subscription — the subscription only maps results into state.
94
101
  */
95
102
  export declare function createChoicesController<TResources extends ResourceMap, TName extends keyof TResources & string>(ctx: ChoicesControllerContext<TResources>, options: ChoicesControllerOptions<TResources, TName>): ChoicesController<RecordOf<TResources, TName>>;
103
+ /** The three choice views of a state, erased to `ChoiceRecord[]`. */
104
+ export interface ChoiceLists {
105
+ allChoices: ChoiceRecord[];
106
+ availableChoices: ChoiceRecord[];
107
+ selectedChoices: ChoiceRecord[];
108
+ }
109
+ /**
110
+ * The state's three choice views as the erased lists an adapter's
111
+ * `ChoicesContextValue` carries — the one place a record becomes a
112
+ * `ChoiceRecord`, so neither adapter writes the cast. The arrays are the
113
+ * state's own, not copies.
114
+ */
115
+ export declare function choiceListsOf<TRecord>(state: ChoicesControllerState<TRecord>): ChoiceLists;
@@ -1,281 +1,321 @@
1
- import { QueryObserver, keepPreviousData } from "@tanstack/query-core";
1
+ import { notifyFetchError } from "../controller/list-controller.js";
2
+ import "../controller/reference-many-controller.js";
2
3
  import { getListOptions, getManyOptions } from "../data/query-options.js";
4
+ import { removeEmpty } from "../data/remove-empty.js";
3
5
  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
- }
6
+ import { QueryObserver, keepPreviousData } from "@tanstack/query-core";
7
+ // src/form/choices-controller.ts
8
+ var DEFAULT_PAGE = 1;
9
+ var DEFAULT_SORT = {
10
+ field: "id",
11
+ order: "DESC"
12
+ };
13
+ /** perPage for the non-id selected getList — generous, real values sets are small. */
14
+ var SELECTED_LIST_PER_PAGE = 1e3;
15
+ /** Normalizes a value into a non-empty identifier list, or `[]` when there is nothing to resolve. */
21
16
  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
- );
17
+ return (Array.isArray(value) ? value : [value]).filter((v) => v != null && v !== "" && (typeof v === "string" || typeof v === "number"));
26
18
  }
19
+ /**
20
+ * Vanilla choices controller: TWO QueryObservers (choices page + selected
21
+ * resolution) mapped into one micro-store, closing charter §11 #16 — selected
22
+ * records resolve by an arbitrary `lookupKey`, not just `id`.
23
+ *
24
+ * Lifecycle (P4 4R): construction is INERT — pure state + closures, zero
25
+ * subscriptions, zero fetching. Acquisition (both observers + KV nothing) runs
26
+ * on the micro-store's FIRST subscriber; the LAST unsubscribe releases both
27
+ * observers and clears the debounce timer. Attach/detach cycles are
28
+ * re-entrant; re-attach rebuilds both observers from CURRENT state.
29
+ * `destroy()` remains the hard imperative teardown for non-React consumers.
30
+ *
31
+ * Loop-prevention: `observer.setOptions` is called ONLY from param-mutating
32
+ * actions (`setPage`/`setSort`/`setFilters`/`setValue`), never from the
33
+ * observer result subscription — the subscription only maps results into state.
34
+ */
27
35
  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
- };
36
+ const reference = options.reference;
37
+ const lookupKey = options.lookupKey ?? "id";
38
+ const isIdLookup = lookupKey === "id";
39
+ const permanentFilter = options.filter ?? {};
40
+ const debounceMs = options.debounceMs ?? 500;
41
+ const store = createMicroStore({
42
+ page: options.page ?? DEFAULT_PAGE,
43
+ perPage: options.perPage ?? 25,
44
+ sort: { ...options.sort ?? DEFAULT_SORT },
45
+ filter: {},
46
+ allChoices: [],
47
+ availableChoices: [],
48
+ selectedChoices: [],
49
+ total: void 0,
50
+ pageInfo: void 0,
51
+ hasNextPage: void 0,
52
+ hasPreviousPage: void 0,
53
+ isPending: true,
54
+ isFetching: false,
55
+ error: null,
56
+ resource: reference,
57
+ source: options.source,
58
+ isFromReference: true
59
+ }, { onSubscribed: acquire });
60
+ let destroyed = false;
61
+ store.onDestroy(() => {
62
+ destroyed = true;
63
+ });
64
+ /** Non-null exactly while attached. */
65
+ let choicesObserver = null;
66
+ /** Selected observer: getMany (id) OR getList (non-id). Rebuilt on setValue. */
67
+ let selectedObserver = null;
68
+ /** The value(s) currently driving selected resolution. */
69
+ let selectedValues = [];
70
+ /** Latest raw fetch results, cached so `recompute()` can re-merge on any change. */
71
+ let pageData;
72
+ let pageTotal;
73
+ let pageInfo;
74
+ let selectedData = [];
75
+ const isGetChoicesEnabled = () => options.enableGetChoices ? options.enableGetChoices(store.getSnapshot().filter) : true;
76
+ const buildChoicesOptions = () => {
77
+ const s = store.getSnapshot();
78
+ return {
79
+ ...getListOptions(ctx, reference, {
80
+ pagination: {
81
+ page: s.page,
82
+ perPage: s.perPage
83
+ },
84
+ sort: s.sort,
85
+ filter: {
86
+ ...s.filter,
87
+ ...permanentFilter
88
+ },
89
+ meta: options.meta
90
+ }),
91
+ placeholderData: keepPreviousData,
92
+ retry: false,
93
+ enabled: isGetChoicesEnabled()
94
+ };
95
+ };
96
+ /**
97
+ * Options for the selected observer. `id` lookup batches through getMany;
98
+ * any other key resolves through a getList filter (single OR array value).
99
+ * Disabled when there is nothing to resolve so it never pins `isPending`.
100
+ */
101
+ const buildSelectedOptions = () => {
102
+ const enabled = selectedValues.length > 0;
103
+ if (isIdLookup) return {
104
+ ...getManyOptions(ctx, reference, {
105
+ ids: selectedValues,
106
+ meta: options.meta
107
+ }),
108
+ retry: false,
109
+ enabled
110
+ };
111
+ const filterValue = selectedValues.length === 1 ? selectedValues[0] : selectedValues;
112
+ return {
113
+ ...getListOptions(ctx, reference, {
114
+ pagination: {
115
+ page: 1,
116
+ perPage: SELECTED_LIST_PER_PAGE
117
+ },
118
+ filter: { [lookupKey]: filterValue },
119
+ meta: options.meta
120
+ }),
121
+ retry: false,
122
+ enabled
123
+ };
124
+ };
125
+ /** Records from the selected observer result, whichever shape it is. */
126
+ const readSelectedData = (result) => {
127
+ const data = result.data;
128
+ if (data == null) return [];
129
+ return Array.isArray(data) ? data : data.data;
130
+ };
131
+ /**
132
+ * Merge the page + selected records into the three views (§0b parity), then
133
+ * write the derived state. Called from BOTH result subscriptions.
134
+ */
135
+ const recompute = (patch) => {
136
+ const s = store.getSnapshot();
137
+ const page = pageData ?? [];
138
+ const selected = selectedData;
139
+ const keyOf = (record) => record[lookupKey];
140
+ const pageKeys = new Set(page.map(keyOf));
141
+ const missing = selected.filter((record) => !pageKeys.has(keyOf(record)));
142
+ const allChoices = missing.length > 0 ? [...missing, ...page] : page;
143
+ let total = pageTotal;
144
+ if (total !== void 0 && missing.length === 1 && selected.length === 1) total = total + 1;
145
+ let hasNextPage;
146
+ let hasPreviousPage;
147
+ if (pageInfo) {
148
+ hasNextPage = pageInfo.hasNextPage;
149
+ hasPreviousPage = pageInfo.hasPreviousPage;
150
+ } else if (pageTotal !== void 0) {
151
+ hasNextPage = s.page * s.perPage < pageTotal;
152
+ hasPreviousPage = s.page > 1;
153
+ }
154
+ store.setState({
155
+ allChoices,
156
+ availableChoices: page,
157
+ selectedChoices: selected,
158
+ total,
159
+ pageInfo,
160
+ hasNextPage,
161
+ hasPreviousPage,
162
+ isPending: patch.isPending,
163
+ isFetching: patch.isFetching,
164
+ error: patch.error
165
+ });
166
+ };
167
+ let lastNotifiedError = null;
168
+ const notifyError = (error) => {
169
+ if (error === lastNotifiedError) return;
170
+ lastNotifiedError = error;
171
+ if (error === null) return;
172
+ notifyFetchError(ctx.notifications, error);
173
+ };
174
+ /**
175
+ * The combined status union. `isPending` reflects the choices query and the
176
+ * selected query ONLY when a value is being resolved — a valueless selected
177
+ * query is disabled and must never pin `isPending` (§0b).
178
+ */
179
+ const onChoicesResult = (result) => {
180
+ const list = result.data;
181
+ pageData = list?.data;
182
+ pageTotal = list?.total;
183
+ pageInfo = list?.pageInfo;
184
+ const selectedPending = selectedValues.length > 0 && selectedObserver ? selectedObserver.getCurrentResult().isPending : false;
185
+ const selectedFetching = selectedValues.length > 0 && selectedObserver ? selectedObserver.getCurrentResult().isFetching : false;
186
+ const error = result.error ?? selectedObserver?.getCurrentResult().error ?? null;
187
+ recompute({
188
+ isPending: result.isPending || selectedPending,
189
+ isFetching: result.isFetching || selectedFetching,
190
+ error
191
+ });
192
+ notifyError(result.error ?? null);
193
+ };
194
+ const onSelectedResult = (result) => {
195
+ selectedData = readSelectedData(result);
196
+ const choicesResult = choicesObserver?.getCurrentResult();
197
+ const choicesPending = choicesResult?.isPending ?? false;
198
+ const choicesFetching = choicesResult?.isFetching ?? false;
199
+ const selectedPending = selectedValues.length > 0 && result.isPending;
200
+ const selectedFetching = selectedValues.length > 0 && result.isFetching;
201
+ const error = choicesResult?.error ?? result.error ?? null;
202
+ recompute({
203
+ isPending: choicesPending || selectedPending,
204
+ isFetching: choicesFetching || selectedFetching,
205
+ error
206
+ });
207
+ };
208
+ const setPage = (page) => {
209
+ if (destroyed) return;
210
+ store.setState({ page });
211
+ choicesObserver?.setOptions(buildChoicesOptions());
212
+ };
213
+ const setPerPage = (perPage) => {
214
+ if (destroyed) return;
215
+ store.setState({
216
+ perPage,
217
+ page: 1
218
+ });
219
+ choicesObserver?.setOptions(buildChoicesOptions());
220
+ };
221
+ const setSort = (sort) => {
222
+ if (destroyed) return;
223
+ store.setState({
224
+ sort: { ...sort },
225
+ page: 1
226
+ });
227
+ choicesObserver?.setOptions(buildChoicesOptions());
228
+ };
229
+ const applyFilters = (filter) => {
230
+ store.setState({
231
+ filter: removeEmpty(filter),
232
+ page: 1
233
+ });
234
+ choicesObserver?.setOptions(buildChoicesOptions());
235
+ };
236
+ let debounceTimer;
237
+ const clearDebounce = () => {
238
+ if (debounceTimer !== void 0) {
239
+ clearTimeout(debounceTimer);
240
+ debounceTimer = void 0;
241
+ }
242
+ };
243
+ store.onDestroy(clearDebounce);
244
+ const setFilters = (filter, debounce = false) => {
245
+ if (destroyed) return;
246
+ clearDebounce();
247
+ if (!debounce) {
248
+ applyFilters(filter);
249
+ return;
250
+ }
251
+ debounceTimer = setTimeout(() => {
252
+ debounceTimer = void 0;
253
+ if (destroyed) return;
254
+ applyFilters(filter);
255
+ }, debounceMs);
256
+ };
257
+ const setValue = (value) => {
258
+ if (destroyed) return;
259
+ const next = toValueList(value);
260
+ if (next.length === selectedValues.length && next.every((v, i) => v === selectedValues[i])) return;
261
+ selectedValues = next;
262
+ if (next.length === 0) selectedData = [];
263
+ selectedObserver?.setOptions(buildSelectedOptions());
264
+ if (!selectedObserver && next.length === 0) recompute({
265
+ isPending: store.getSnapshot().isPending,
266
+ isFetching: store.getSnapshot().isFetching,
267
+ error: store.getSnapshot().error
268
+ });
269
+ };
270
+ const refetch = () => {
271
+ if (destroyed) return;
272
+ choicesObserver?.refetch();
273
+ if (selectedValues.length > 0) selectedObserver?.refetch();
274
+ };
275
+ /**
276
+ * Attach: runs on the micro-store's FIRST subscriber. Creates and subscribes
277
+ * BOTH observers with options built from CURRENT state, and returns the
278
+ * teardown the micro-store runs on last-unsubscribe AND destroy-while-attached.
279
+ */
280
+ function acquire() {
281
+ const choices = new QueryObserver(ctx.queryClient, buildChoicesOptions());
282
+ choicesObserver = choices;
283
+ const selected = new QueryObserver(ctx.queryClient, buildSelectedOptions());
284
+ selectedObserver = selected;
285
+ const unsubChoices = choices.subscribe(onChoicesResult);
286
+ const unsubSelected = selected.subscribe(onSelectedResult);
287
+ onSelectedResult(selected.getCurrentResult());
288
+ onChoicesResult(choices.getCurrentResult());
289
+ return () => {
290
+ unsubChoices();
291
+ unsubSelected();
292
+ choicesObserver = null;
293
+ selectedObserver = null;
294
+ clearDebounce();
295
+ };
296
+ }
297
+ return {
298
+ ...store,
299
+ setPage,
300
+ setPerPage,
301
+ setSort,
302
+ setFilters,
303
+ setValue,
304
+ refetch
305
+ };
278
306
  }
279
- export {
280
- createChoicesController
281
- };
307
+ /**
308
+ * The state's three choice views as the erased lists an adapter's
309
+ * `ChoicesContextValue` carries — the one place a record becomes a
310
+ * `ChoiceRecord`, so neither adapter writes the cast. The arrays are the
311
+ * state's own, not copies.
312
+ */
313
+ function choiceListsOf(state) {
314
+ const { allChoices, availableChoices, selectedChoices } = state;
315
+ return {
316
+ allChoices,
317
+ availableChoices,
318
+ selectedChoices
319
+ };
320
+ }
321
+ export { choiceListsOf, createChoicesController };