@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.
- package/dist/assistant/assistant-session.d.ts +6 -0
- package/dist/assistant/assistant-session.js +204 -181
- package/dist/assistant/echo-transport.js +26 -29
- package/dist/assistant/http-transport.d.ts +10 -0
- package/dist/assistant/http-transport.js +98 -0
- package/dist/assistant/types.d.ts +1 -7
- package/dist/auth/access-control.d.ts +13 -5
- package/dist/auth/access-control.js +25 -21
- package/dist/auth/auth-options.d.ts +23 -17
- package/dist/auth/auth-options.js +82 -48
- package/dist/auth/permission-matrix.d.ts +12 -0
- package/dist/auth/permission-matrix.js +67 -0
- package/dist/auth/types.d.ts +14 -4
- package/dist/block-editor/block-document.d.ts +64 -0
- package/dist/block-editor/block-document.js +147 -0
- package/dist/block-editor/data-blocks.d.ts +63 -0
- package/dist/block-editor/data-blocks.js +117 -0
- package/dist/chart/chart-data.js +46 -44
- package/dist/command-menu/hotkey.js +58 -47
- package/dist/command-menu/recents.d.ts +4 -0
- package/dist/command-menu/recents.js +12 -11
- package/dist/command-menu/registry.d.ts +9 -5
- package/dist/command-menu/registry.js +36 -46
- package/dist/controller/calendar-dates.d.ts +12 -0
- package/dist/controller/calendar-dates.js +94 -113
- package/dist/controller/calendar-events.js +93 -88
- package/dist/controller/client-list-controller.d.ts +2 -2
- package/dist/controller/client-list-controller.js +251 -254
- package/dist/controller/data-table-columns.d.ts +2 -0
- package/dist/controller/data-table-columns.js +50 -56
- package/dist/controller/deep-equal.d.ts +1 -0
- package/dist/controller/deep-equal.js +14 -0
- package/dist/controller/filter-toggle.js +36 -44
- package/dist/controller/in-place-editor.d.ts +43 -0
- package/dist/controller/in-place-editor.js +43 -0
- package/dist/controller/infinite-list-controller.js +219 -189
- package/dist/controller/inspector.d.ts +55 -0
- package/dist/controller/inspector.js +70 -0
- package/dist/controller/job-monitor.d.ts +10 -4
- package/dist/controller/job-monitor.js +46 -47
- package/dist/controller/kanban-board.js +45 -24
- package/dist/controller/list-controller.d.ts +8 -5
- package/dist/controller/list-controller.js +447 -400
- package/dist/controller/list-params.js +96 -87
- package/dist/controller/pivot-grid.d.ts +4 -4
- package/dist/controller/pivot-grid.js +89 -95
- package/dist/controller/prev-next.d.ts +2 -2
- package/dist/controller/prev-next.js +74 -54
- package/dist/controller/record-controllers.d.ts +14 -6
- package/dist/controller/record-controllers.js +288 -257
- package/dist/controller/record-diff.d.ts +21 -0
- package/dist/controller/record-diff.js +50 -0
- package/dist/controller/record-timeline.d.ts +5 -5
- package/dist/controller/record-timeline.js +42 -30
- package/dist/controller/record-tree.js +57 -47
- package/dist/controller/record-workflow.d.ts +38 -16
- package/dist/controller/record-workflow.js +85 -64
- package/dist/controller/reference-many-controller.d.ts +2 -0
- package/dist/controller/reference-many-controller.js +199 -192
- package/dist/controller/reference-one-controller.d.ts +1 -1
- package/dist/controller/reference-one-controller.js +63 -56
- package/dist/controller/saved-queries.js +104 -91
- package/dist/controller/translate-fallback.js +10 -6
- package/dist/csv/export.d.ts +4 -3
- package/dist/csv/export.js +43 -40
- package/dist/csv/import-controller.d.ts +4 -0
- package/dist/csv/import-controller.js +189 -169
- package/dist/csv/match-headers.js +29 -22
- package/dist/csv/parse-with.js +60 -54
- package/dist/csv/run-import.js +69 -72
- package/dist/csv/validate-rows.d.ts +1 -1
- package/dist/csv/validate-rows.js +65 -52
- package/dist/data/cache-ops.d.ts +5 -4
- package/dist/data/cache-ops.js +208 -188
- package/dist/data/combine-data-providers.js +25 -23
- package/dist/data/data-provider-log.d.ts +45 -0
- package/dist/data/data-provider-log.js +93 -0
- package/dist/data/errors.js +14 -10
- package/dist/data/fake-provider.d.ts +6 -3
- package/dist/data/fake-provider.js +135 -114
- package/dist/data/fetch-utils.js +61 -44
- package/dist/data/filter-branch.d.ts +5 -0
- package/dist/data/filter-branch.js +10 -0
- package/dist/data/get-many-aggregate.js +84 -76
- package/dist/data/http-error.js +17 -12
- package/dist/data/keys.js +50 -36
- package/dist/data/mutation-options.d.ts +14 -9
- package/dist/data/mutation-options.js +93 -109
- package/dist/data/query-options.js +73 -68
- package/dist/data/remove-empty.d.ts +3 -0
- package/dist/data/remove-empty.js +11 -0
- package/dist/data/undoable.d.ts +2 -2
- package/dist/data/undoable.js +190 -174
- package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
- package/dist/data/with-lifecycle-callbacks.js +66 -52
- package/dist/environment/application-update-store.js +58 -55
- package/dist/environment/offline-store.js +21 -21
- package/dist/form/array-binding.d.ts +30 -11
- package/dist/form/array-binding.js +184 -166
- package/dist/form/choice-ids.d.ts +7 -0
- package/dist/form/choice-ids.js +11 -0
- package/dist/form/choices-controller.d.ts +22 -2
- package/dist/form/choices-controller.js +315 -275
- package/dist/form/date-format.d.ts +38 -0
- package/dist/form/date-format.js +89 -0
- package/dist/form/dirty-values.d.ts +2 -2
- package/dist/form/dirty-values.js +22 -11
- package/dist/form/duration.d.ts +22 -0
- package/dist/form/duration.js +49 -0
- package/dist/form/edit-create-form.d.ts +12 -5
- package/dist/form/edit-create-form.js +243 -188
- package/dist/form/event-value.d.ts +6 -0
- package/dist/form/event-value.js +17 -0
- package/dist/form/field-error.d.ts +14 -0
- package/dist/form/field-error.js +28 -0
- package/dist/form/field-name.d.ts +20 -0
- package/dist/form/field-name.js +6 -0
- package/dist/form/filter-form-values.js +63 -49
- package/dist/form/form-controller.d.ts +9 -1
- package/dist/form/form-controller.js +165 -160
- package/dist/form/form-groups.d.ts +4 -3
- package/dist/form/form-groups.js +79 -79
- package/dist/form/form-handle.d.ts +175 -0
- package/dist/form/form-handle.js +68 -0
- package/dist/form/input-binding.d.ts +56 -31
- package/dist/form/input-binding.js +171 -118
- package/dist/form/input-values.d.ts +140 -0
- package/dist/form/input-values.js +206 -0
- package/dist/form/json-text.d.ts +11 -0
- package/dist/form/json-text.js +62 -37
- package/dist/form/save-pipeline.d.ts +6 -8
- package/dist/form/save-pipeline.js +91 -82
- package/dist/form/set-form-values.d.ts +6 -0
- package/dist/form/set-form-values.js +10 -0
- package/dist/form/source.js +97 -65
- package/dist/form/text-of.d.ts +2 -0
- package/dist/form/text-of.js +7 -0
- package/dist/form/validators.d.ts +58 -19
- package/dist/form/validators.js +97 -71
- package/dist/geo/bbox.d.ts +5 -4
- package/dist/geo/bbox.js +69 -48
- package/dist/geo/bounds.d.ts +14 -3
- package/dist/geo/bounds.js +98 -68
- package/dist/geo/constraints.js +88 -93
- package/dist/geo/coordinates.d.ts +10 -0
- package/dist/geo/coordinates.js +44 -23
- package/dist/geo/editor.d.ts +13 -13
- package/dist/geo/editor.js +176 -153
- package/dist/geo/geocoding.d.ts +4 -4
- package/dist/geo/geocoding.js +38 -41
- package/dist/geo/geojson.d.ts +12 -74
- package/dist/geo/geojson.js +52 -40
- package/dist/geo/geometry-math.d.ts +4 -4
- package/dist/geo/geometry-math.js +100 -93
- package/dist/geo/index.d.ts +9 -8
- package/dist/geo/map-layers.d.ts +42 -7
- package/dist/geo/map-layers.js +88 -62
- package/dist/geo/map-style.d.ts +12 -0
- package/dist/geo/map-style.js +43 -32
- package/dist/geo/messages.js +48 -45
- package/dist/geo/nominatim.js +88 -80
- package/dist/geo/osm-presets.d.ts +4 -3
- package/dist/geo/osm-presets.js +108 -86
- package/dist/geo/osm-tags.js +14 -12
- package/dist/geo/overpass.d.ts +14 -2
- package/dist/geo/overpass.js +83 -90
- package/dist/geo/shape-value.d.ts +9 -5
- package/dist/geo/shape-value.js +169 -146
- package/dist/geo/snap-to-roads.d.ts +3 -3
- package/dist/geo/snap-to-roads.js +55 -50
- package/dist/geo/throttle.js +36 -39
- package/dist/guards.d.ts +44 -0
- package/dist/guards.js +77 -0
- package/dist/i18n/default-messages.js +34 -29
- package/dist/i18n/english-messages.d.ts +172 -0
- package/dist/i18n/english-messages.js +359 -189
- package/dist/i18n/i18n.d.ts +2 -2
- package/dist/i18n/i18n.js +47 -60
- package/dist/i18n/inflection.d.ts +34 -0
- package/dist/i18n/inflection.js +82 -0
- package/dist/i18n/messages.d.ts +10 -2
- package/dist/i18n/messages.js +17 -4
- package/dist/i18n/missing-keys.d.ts +4 -0
- package/dist/i18n/missing-keys.js +70 -62
- package/dist/i18n/translatable.d.ts +16 -1
- package/dist/i18n/translatable.js +40 -47
- package/dist/index.d.ts +87 -39
- package/dist/index.js +54 -301
- package/dist/json/deep-equal.d.ts +8 -0
- package/dist/json/deep-equal.js +24 -0
- package/dist/json/strip-json-comments.js +38 -42
- package/dist/noop.d.ts +2 -0
- package/dist/noop.js +4 -0
- package/dist/notification/error-message.d.ts +2 -0
- package/dist/notification/error-message.js +10 -0
- package/dist/notification/notification-store.d.ts +3 -2
- package/dist/notification/notification-store.js +41 -45
- package/dist/onboarding-tour/placement.d.ts +2 -0
- package/dist/onboarding-tour/placement.js +65 -78
- package/dist/onboarding-tour/tour-controller.js +46 -54
- package/dist/realtime/add-events-for-mutations.js +75 -81
- package/dist/realtime/announce-lock.js +16 -13
- package/dist/realtime/broadcast-channel-transport.js +65 -59
- package/dist/realtime/copy-members.js +22 -14
- package/dist/realtime/in-memory-lock-provider.js +40 -48
- package/dist/realtime/live.js +95 -101
- package/dist/realtime/lock-options.js +16 -16
- package/dist/realtime/lock-session.js +91 -78
- package/dist/realtime/memory-transport.js +25 -25
- package/dist/realtime/presence.js +120 -122
- package/dist/realtime/realtime-data-provider.d.ts +10 -0
- package/dist/realtime/realtime-data-provider.js +45 -42
- package/dist/realtime/reconnect-backoff.js +17 -0
- package/dist/realtime/sse-transport.d.ts +31 -0
- package/dist/realtime/sse-transport.js +235 -0
- package/dist/realtime/status-store.js +23 -28
- package/dist/realtime/topic-hub.js +62 -71
- package/dist/realtime/topics.js +10 -12
- package/dist/realtime/types.js +10 -12
- package/dist/realtime/unread-count.js +21 -21
- package/dist/realtime/websocket-transport.d.ts +32 -0
- package/dist/realtime/websocket-transport.js +306 -0
- package/dist/resource/describe-resource.d.ts +2 -0
- package/dist/resource/describe-resource.js +247 -219
- package/dist/resource/get-at-path.js +29 -14
- package/dist/resource/identifier.d.ts +18 -0
- package/dist/resource/identifier.js +27 -0
- package/dist/resource/record.d.ts +4 -0
- package/dist/resource/record.js +10 -0
- package/dist/resource/register.d.ts +7 -0
- package/dist/resource/registry-store.d.ts +45 -15
- package/dist/resource/registry-store.js +67 -60
- package/dist/resource/registry.d.ts +10 -0
- package/dist/resource/registry.js +8 -4
- package/dist/resource/source-path.d.ts +23 -2
- package/dist/rich-text/rich-text-extras.d.ts +134 -0
- package/dist/rich-text/rich-text-extras.js +181 -0
- package/dist/rich-text/rich-text-image.d.ts +124 -0
- package/dist/rich-text/rich-text-image.js +163 -0
- package/dist/rich-text/rich-text.d.ts +6 -0
- package/dist/rich-text/rich-text.js +125 -88
- package/dist/router/location.d.ts +24 -4
- package/dist/router/location.js +43 -18
- package/dist/router/match.js +47 -39
- package/dist/router/paths.js +40 -34
- package/dist/router/record-from-location.d.ts +1 -1
- package/dist/router/record-from-location.js +25 -17
- package/dist/store/adapters.js +51 -55
- package/dist/store/micro-store.js +57 -59
- package/dist/store/shallow-equal.d.ts +12 -0
- package/dist/store/shallow-equal.js +20 -0
- package/dist/store/store.js +54 -57
- package/dist/ui-defaults.d.ts +15 -0
- package/dist/ui-defaults.js +17 -0
- package/package.json +13 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isPlainObject } from "../guards.js";
|
|
2
|
+
// src/controller/deep-equal.ts
|
|
3
|
+
function deepEqual(a, b) {
|
|
4
|
+
if (Object.is(a, b)) return true;
|
|
5
|
+
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
|
|
6
|
+
if (a instanceof Date) return b instanceof Date && Object.is(+a, +b);
|
|
7
|
+
if (isPlainObject(a)) {
|
|
8
|
+
if (!isPlainObject(b)) return false;
|
|
9
|
+
const keys = Object.keys(a);
|
|
10
|
+
return keys.length === Object.keys(b).length && keys.every((key) => key in b && deepEqual(a[key], b[key]));
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
export { deepEqual };
|
|
@@ -1,52 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (a instanceof Date) return b instanceof Date && +a === +b;
|
|
12
|
-
if (isPlainObject(a)) {
|
|
13
|
-
if (!isPlainObject(b)) return false;
|
|
14
|
-
const keys = Object.keys(a);
|
|
15
|
-
return keys.length === Object.keys(b).length && keys.every((key) => key in b && deepEqual(a[key], b[key]));
|
|
16
|
-
}
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
1
|
+
import { isPlainObject } from "../guards.js";
|
|
2
|
+
import { deepEqual } from "./deep-equal.js";
|
|
3
|
+
// src/controller/filter-toggle.ts
|
|
4
|
+
/**
|
|
5
|
+
* The select and toggle rules behind a one-click list filter (ra-core's
|
|
6
|
+
* `FilterListItem` and `ToggleFilterButton`), pure so both adapters share them.
|
|
7
|
+
*
|
|
8
|
+
* Chelone divergence: arrays compare for deep equality, not lodash's
|
|
9
|
+
* contains-all rule.
|
|
10
|
+
*/
|
|
19
11
|
function matches(expected, actual) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(key) => expected[key] === void 0 || matches(expected[key], actual[key])
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return deepEqual(expected, actual);
|
|
12
|
+
if (isPlainObject(expected)) return isPlainObject(actual) && Object.keys(expected).every((key) => expected[key] === void 0 || matches(expected[key], actual[key]));
|
|
13
|
+
return deepEqual(expected, actual);
|
|
26
14
|
}
|
|
15
|
+
/** True when every defined key of `value` is held by `filters`. */
|
|
27
16
|
function isFilterSelected(value, filters) {
|
|
28
|
-
|
|
17
|
+
return matches(value, filters);
|
|
29
18
|
}
|
|
30
19
|
function omitValue(filters, value) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
return next;
|
|
20
|
+
const next = { ...filters };
|
|
21
|
+
for (const key of Object.keys(value)) {
|
|
22
|
+
if (value[key] === void 0) continue;
|
|
23
|
+
const held = next[key];
|
|
24
|
+
if (isPlainObject(value[key]) && isPlainObject(held)) {
|
|
25
|
+
const rest = omitValue(held, value[key]);
|
|
26
|
+
if (Object.keys(rest).length > 0) next[key] = rest;
|
|
27
|
+
else delete next[key];
|
|
28
|
+
} else delete next[key];
|
|
29
|
+
}
|
|
30
|
+
return next;
|
|
44
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Selecting a value merges it into the filters; toggling a selected one drops
|
|
34
|
+
* the keys it holds, down to nested sub-keys, and keeps the rest. Neither
|
|
35
|
+
* argument is mutated.
|
|
36
|
+
*/
|
|
45
37
|
function toggleFilterValue(value, filters) {
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
if (!isFilterSelected(value, filters)) return {
|
|
39
|
+
...filters,
|
|
40
|
+
...value
|
|
41
|
+
};
|
|
42
|
+
return omitValue(filters, value);
|
|
48
43
|
}
|
|
49
|
-
export {
|
|
50
|
-
isFilterSelected,
|
|
51
|
-
toggleFilterValue
|
|
52
|
-
};
|
|
44
|
+
export { isFilterSelected, toggleFilterValue };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rules for an in-place editor. No react, no vue: both InPlaceEditor
|
|
3
|
+
* adapters step through the same states and send the same fields on a save.
|
|
4
|
+
*/
|
|
5
|
+
import type { LooseRecord, ResourceRecord } from "../resource/record.js";
|
|
6
|
+
export type InPlaceEditorState<R extends ResourceRecord = LooseRecord> = {
|
|
7
|
+
status: "reading";
|
|
8
|
+
} | {
|
|
9
|
+
status: "editing";
|
|
10
|
+
} | {
|
|
11
|
+
status: "saving";
|
|
12
|
+
preview: R;
|
|
13
|
+
};
|
|
14
|
+
export type InPlaceEditorEvent<R extends ResourceRecord = LooseRecord> = {
|
|
15
|
+
type: "edit";
|
|
16
|
+
} | {
|
|
17
|
+
type: "cancel";
|
|
18
|
+
} | {
|
|
19
|
+
type: "save";
|
|
20
|
+
preview: R;
|
|
21
|
+
} | {
|
|
22
|
+
type: "settled";
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Steps the editor: reading to editing on `edit`, editing to reading on
|
|
26
|
+
* `cancel`, editing to saving on `save`, saving to reading on `settled`. Any
|
|
27
|
+
* other pair returns the same state object, so a click while saving does nothing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function nextInPlaceEditorState<R extends ResourceRecord = LooseRecord>(state: InPlaceEditorState<R>, event: InPlaceEditorEvent<R>): InPlaceEditorState<R>;
|
|
30
|
+
export interface InPlaceSave<R extends ResourceRecord> {
|
|
31
|
+
id: R["id"];
|
|
32
|
+
data: Partial<R>;
|
|
33
|
+
previousData: R;
|
|
34
|
+
/** The record with the changed fields over it. */
|
|
35
|
+
preview: R;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Plans a save from the record and the dirty form values. `data` keeps the
|
|
39
|
+
* fields whose value is not deeply equal to the record's, so a value typed and
|
|
40
|
+
* then typed back is dropped. Returns `null` when nothing changed, so a save
|
|
41
|
+
* with no change makes no call.
|
|
42
|
+
*/
|
|
43
|
+
export declare function planInPlaceSave<R extends ResourceRecord>(record: R, dirty: Record<string, unknown>): InPlaceSave<R> | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isLooseRecord } from "../resource/record.js";
|
|
2
|
+
import { deepEqual } from "./deep-equal.js";
|
|
3
|
+
// src/controller/in-place-editor.ts
|
|
4
|
+
/**
|
|
5
|
+
* Steps the editor: reading to editing on `edit`, editing to reading on
|
|
6
|
+
* `cancel`, editing to saving on `save`, saving to reading on `settled`. Any
|
|
7
|
+
* other pair returns the same state object, so a click while saving does nothing.
|
|
8
|
+
*/
|
|
9
|
+
function nextInPlaceEditorState(state, event) {
|
|
10
|
+
switch (state.status) {
|
|
11
|
+
case "reading": return event.type === "edit" ? { status: "editing" } : state;
|
|
12
|
+
case "editing":
|
|
13
|
+
if (event.type === "cancel") return { status: "reading" };
|
|
14
|
+
if (event.type === "save") return {
|
|
15
|
+
status: "saving",
|
|
16
|
+
preview: event.preview
|
|
17
|
+
};
|
|
18
|
+
return state;
|
|
19
|
+
case "saving": return event.type === "settled" ? { status: "reading" } : state;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Plans a save from the record and the dirty form values. `data` keeps the
|
|
24
|
+
* fields whose value is not deeply equal to the record's, so a value typed and
|
|
25
|
+
* then typed back is dropped. Returns `null` when nothing changed, so a save
|
|
26
|
+
* with no change makes no call.
|
|
27
|
+
*/
|
|
28
|
+
function planInPlaceSave(record, dirty) {
|
|
29
|
+
if (!isLooseRecord(record)) return null;
|
|
30
|
+
const changed = Object.entries(dirty).filter(([key, value]) => !deepEqual(record[key], value));
|
|
31
|
+
if (changed.length === 0) return null;
|
|
32
|
+
const data = Object.fromEntries(changed);
|
|
33
|
+
return {
|
|
34
|
+
id: record.id,
|
|
35
|
+
data,
|
|
36
|
+
previousData: record,
|
|
37
|
+
preview: {
|
|
38
|
+
...record,
|
|
39
|
+
...data
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export { nextInPlaceEditorState, planInPlaceSave };
|
|
@@ -1,193 +1,223 @@
|
|
|
1
|
-
import { QueryClient, InfiniteQueryObserver } from "@tanstack/query-core";
|
|
2
1
|
import { getInfiniteListOptions, getListOptions } from "../data/query-options.js";
|
|
3
2
|
import { createMicroStore } from "../store/micro-store.js";
|
|
4
|
-
import {
|
|
3
|
+
import { createErrorAnnouncer, createListController, runSelectAll } from "./list-controller.js";
|
|
4
|
+
import { InfiniteQueryObserver, QueryClient } from "@tanstack/query-core";
|
|
5
|
+
// src/controller/infinite-list-controller.ts
|
|
6
|
+
/**
|
|
7
|
+
* Vanilla infinite list controller: list params and selection from an inner
|
|
8
|
+
* {@link createListController} (disabled, so it never fetches), data from its
|
|
9
|
+
* own InfiniteQueryObserver that gathers every loaded page into one list.
|
|
10
|
+
*
|
|
11
|
+
* Lifecycle (Task 4R): construction is INERT. The first subscriber acquires
|
|
12
|
+
* the inner controller and the observer, the last unsubscribe releases both,
|
|
13
|
+
* and `destroy()` is the hard teardown. Re-attach rebuilds the query options
|
|
14
|
+
* from the inner controller's CURRENT params.
|
|
15
|
+
*
|
|
16
|
+
* Loop-prevention rule, as in `createListController`: `observer.setOptions`
|
|
17
|
+
* runs ONLY when the inner params change, never from the observer result.
|
|
18
|
+
* The query key holds the start page, so any param change, `setPage`
|
|
19
|
+
* included, drops the loaded pages and loads from the new start page. The
|
|
20
|
+
* out-of-range page reset of the list controller does not apply: the inner
|
|
21
|
+
* controller reads a private empty cache, so no result can trigger it.
|
|
22
|
+
*/
|
|
5
23
|
function createInfiniteListController(ctx, resource, options = {}) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
24
|
+
const innerCache = new QueryClient({ defaultOptions: { queries: { gcTime: 0 } } });
|
|
25
|
+
const inner = createListController({
|
|
26
|
+
...ctx,
|
|
27
|
+
queryClient: innerCache,
|
|
28
|
+
notifications: void 0
|
|
29
|
+
}, resource, {
|
|
30
|
+
...options,
|
|
31
|
+
enabled: false
|
|
32
|
+
});
|
|
33
|
+
const paramsOf = () => {
|
|
34
|
+
const { page, perPage, sort, filter, displayedFilters, selectedIds } = inner.getSnapshot();
|
|
35
|
+
return {
|
|
36
|
+
page,
|
|
37
|
+
perPage,
|
|
38
|
+
sort,
|
|
39
|
+
filter,
|
|
40
|
+
displayedFilters,
|
|
41
|
+
selectedIds
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const store = createMicroStore({
|
|
45
|
+
...paramsOf(),
|
|
46
|
+
data: void 0,
|
|
47
|
+
total: void 0,
|
|
48
|
+
pageInfo: void 0,
|
|
49
|
+
isPending: true,
|
|
50
|
+
isFetching: false,
|
|
51
|
+
isFetchingNextPage: false,
|
|
52
|
+
isFetchingPreviousPage: false,
|
|
53
|
+
error: null,
|
|
54
|
+
hasNextPage: void 0,
|
|
55
|
+
hasPreviousPage: void 0
|
|
56
|
+
}, { onSubscribed: acquire });
|
|
57
|
+
let destroyed = false;
|
|
58
|
+
store.onDestroy(() => {
|
|
59
|
+
destroyed = true;
|
|
60
|
+
inner.destroy();
|
|
61
|
+
innerCache.clear();
|
|
62
|
+
});
|
|
63
|
+
/** Non-null exactly while attached. */
|
|
64
|
+
let observer = null;
|
|
65
|
+
/** The params the observer's options were last built from. */
|
|
66
|
+
let applied = null;
|
|
67
|
+
/**
|
|
68
|
+
* The page load or refetch this controller has running. A second load
|
|
69
|
+
* started on the same query would cancel the first (query-core's default),
|
|
70
|
+
* so another load waits for it instead.
|
|
71
|
+
*/
|
|
72
|
+
let busy = null;
|
|
73
|
+
/** Sort and filter of a request: the infinite fetch and `selectAll` share them. */
|
|
74
|
+
const requestParams = (s) => ({
|
|
75
|
+
sort: s.sort.length === 1 ? s.sort[0] : s.sort,
|
|
76
|
+
filter: {
|
|
77
|
+
...s.filter,
|
|
78
|
+
...options.filter ?? {}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const buildOptions = () => {
|
|
82
|
+
const s = inner.getSnapshot();
|
|
83
|
+
applied = s;
|
|
84
|
+
return {
|
|
85
|
+
...getInfiniteListOptions(ctx, resource, {
|
|
86
|
+
pagination: {
|
|
87
|
+
page: s.page,
|
|
88
|
+
perPage: s.perPage
|
|
89
|
+
},
|
|
90
|
+
...requestParams(s)
|
|
91
|
+
}),
|
|
92
|
+
retry: false,
|
|
93
|
+
enabled: options.enabled ?? true
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
/** Params and selection live in the inner controller; this copies them over. */
|
|
97
|
+
const mirror = () => {
|
|
98
|
+
store.setState(paramsOf());
|
|
99
|
+
};
|
|
100
|
+
/** The ONLY path that reconfigures the observer: the inner params changed. */
|
|
101
|
+
const onInner = () => {
|
|
102
|
+
mirror();
|
|
103
|
+
if (!observer || !applied) return;
|
|
104
|
+
const next = inner.getSnapshot();
|
|
105
|
+
if (next.page !== applied.page || next.perPage !== applied.perPage || next.sort !== applied.sort || next.filter !== applied.filter) {
|
|
106
|
+
busy = null;
|
|
107
|
+
observer.setOptions(buildOptions());
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const errors = createErrorAnnouncer(ctx.notifications);
|
|
111
|
+
let lastPages;
|
|
112
|
+
let lastRows;
|
|
113
|
+
const onResult = (result) => {
|
|
114
|
+
const pages = result.data?.pages;
|
|
115
|
+
if (pages !== lastPages) {
|
|
116
|
+
lastPages = pages;
|
|
117
|
+
lastRows = pages?.flatMap((page) => page.data);
|
|
118
|
+
}
|
|
119
|
+
const last = pages?.[pages.length - 1];
|
|
120
|
+
store.setState({
|
|
121
|
+
data: lastRows,
|
|
122
|
+
total: last?.total,
|
|
123
|
+
pageInfo: last?.pageInfo,
|
|
124
|
+
isPending: result.isPending,
|
|
125
|
+
isFetching: result.isFetching,
|
|
126
|
+
isFetchingNextPage: result.isFetchingNextPage,
|
|
127
|
+
isFetchingPreviousPage: result.isFetchingPreviousPage,
|
|
128
|
+
error: result.error ?? null,
|
|
129
|
+
hasNextPage: pages ? result.hasNextPage : void 0,
|
|
130
|
+
hasPreviousPage: pages ? result.hasPreviousPage : void 0
|
|
131
|
+
});
|
|
132
|
+
if (result.error) errors.announce(result.error);
|
|
133
|
+
else errors.rearm();
|
|
134
|
+
};
|
|
135
|
+
function acquire() {
|
|
136
|
+
const unsubscribeInner = inner.subscribe(onInner);
|
|
137
|
+
mirror();
|
|
138
|
+
const attached = new InfiniteQueryObserver(ctx.queryClient, buildOptions());
|
|
139
|
+
observer = attached;
|
|
140
|
+
const unsubscribeObserver = attached.subscribe(onResult);
|
|
141
|
+
onResult(attached.getCurrentResult());
|
|
142
|
+
return () => {
|
|
143
|
+
unsubscribeObserver();
|
|
144
|
+
unsubscribeInner();
|
|
145
|
+
observer = null;
|
|
146
|
+
applied = null;
|
|
147
|
+
lastPages = void 0;
|
|
148
|
+
lastRows = void 0;
|
|
149
|
+
busy = null;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const through = (action) => (...args) => {
|
|
153
|
+
const result = action(...args);
|
|
154
|
+
if (!destroyed) mirror();
|
|
155
|
+
return result;
|
|
156
|
+
};
|
|
157
|
+
/** Records `promise` as the running load until it settles. */
|
|
158
|
+
const track = (kind, promise) => {
|
|
159
|
+
const entry = {
|
|
160
|
+
kind,
|
|
161
|
+
promise
|
|
162
|
+
};
|
|
163
|
+
busy = entry;
|
|
164
|
+
promise.then(() => {
|
|
165
|
+
if (busy === entry) busy = null;
|
|
166
|
+
});
|
|
167
|
+
return promise;
|
|
168
|
+
};
|
|
169
|
+
const swallow = () => void 0;
|
|
170
|
+
const fetchMore = (direction) => {
|
|
171
|
+
if (destroyed || !observer) return Promise.resolve();
|
|
172
|
+
if (busy) {
|
|
173
|
+
if (busy.kind === direction) return busy.promise;
|
|
174
|
+
return busy.promise.then(() => fetchMore(direction));
|
|
175
|
+
}
|
|
176
|
+
const current = observer.getCurrentResult();
|
|
177
|
+
if (direction === "next" ? !current.hasNextPage : !current.hasPreviousPage) return Promise.resolve();
|
|
178
|
+
const started = direction === "next" ? observer.fetchNextPage() : observer.fetchPreviousPage();
|
|
179
|
+
return track(direction, started.then(swallow, swallow));
|
|
180
|
+
};
|
|
181
|
+
const refetch = () => {
|
|
182
|
+
if (destroyed || !observer) return;
|
|
183
|
+
if (busy) {
|
|
184
|
+
if (busy.kind !== "refetch") busy.promise.then(refetch);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
track("refetch", observer.refetch().then(swallow, swallow));
|
|
188
|
+
};
|
|
189
|
+
const selectAll = (opts) => runSelectAll(ctx.notifications, async (limit) => {
|
|
190
|
+
const s = inner.getSnapshot();
|
|
191
|
+
return (await ctx.queryClient.fetchQuery({
|
|
192
|
+
...getListOptions(ctx, resource, {
|
|
193
|
+
pagination: {
|
|
194
|
+
page: 1,
|
|
195
|
+
perPage: limit
|
|
196
|
+
},
|
|
197
|
+
...requestParams(s)
|
|
198
|
+
}),
|
|
199
|
+
retry: false
|
|
200
|
+
})).data;
|
|
201
|
+
}, (ids) => {
|
|
202
|
+
inner.select(ids);
|
|
203
|
+
mirror();
|
|
204
|
+
}, () => destroyed, opts?.limit);
|
|
205
|
+
return {
|
|
206
|
+
...store,
|
|
207
|
+
setPage: through(inner.setPage),
|
|
208
|
+
setPerPage: through(inner.setPerPage),
|
|
209
|
+
setSort: through(inner.setSort),
|
|
210
|
+
setFilters: through(inner.setFilters),
|
|
211
|
+
setParams: through(inner.setParams),
|
|
212
|
+
showFilter: through(inner.showFilter),
|
|
213
|
+
hideFilter: through(inner.hideFilter),
|
|
214
|
+
select: through(inner.select),
|
|
215
|
+
toggleItem: through(inner.toggleItem),
|
|
216
|
+
unselectAll: through(inner.unselectAll),
|
|
217
|
+
selectAll,
|
|
218
|
+
refetch,
|
|
219
|
+
fetchNextPage: () => fetchMore("next"),
|
|
220
|
+
fetchPreviousPage: () => fetchMore("previous")
|
|
221
|
+
};
|
|
190
222
|
}
|
|
191
|
-
export {
|
|
192
|
-
createInfiniteListController
|
|
193
|
-
};
|
|
223
|
+
export { createInfiniteListController };
|