@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.
- 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 +21 -17
- package/dist/auth/auth-options.js +80 -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 +60 -0
- package/dist/block-editor/block-document.js +143 -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.js +8 -11
- package/dist/command-menu/registry.d.ts +9 -5
- package/dist/command-menu/registry.js +36 -46
- package/dist/controller/calendar-dates.js +82 -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 +42 -0
- package/dist/controller/in-place-editor.js +42 -0
- package/dist/controller/infinite-list-controller.js +219 -189
- package/dist/controller/inspector.d.ts +49 -0
- package/dist/controller/inspector.js +64 -0
- package/dist/controller/job-monitor.d.ts +3 -4
- package/dist/controller/job-monitor.js +37 -47
- package/dist/controller/kanban-board.d.ts +2 -3
- package/dist/controller/kanban-board.js +43 -24
- package/dist/controller/list-controller.d.ts +5 -5
- package/dist/controller/list-controller.js +444 -400
- package/dist/controller/list-params.js +96 -87
- 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.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.js +198 -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 +2 -1
- package/dist/csv/export.js +43 -40
- package/dist/csv/import-controller.js +185 -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 +129 -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 +3 -2
- package/dist/form/choices-controller.js +302 -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 +169 -118
- package/dist/form/input-values.d.ts +130 -0
- package/dist/form/input-values.js +177 -0
- package/dist/form/json-text.js +51 -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.js +31 -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 +7 -6
- package/dist/geo/map-layers.d.ts +42 -7
- package/dist/geo/map-layers.js +88 -62
- package/dist/geo/map-style.js +31 -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 +37 -0
- package/dist/guards.js +62 -0
- package/dist/i18n/default-messages.js +34 -29
- package/dist/i18n/english-messages.js +187 -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.js +66 -62
- package/dist/i18n/translatable.d.ts +16 -1
- package/dist/i18n/translatable.js +40 -47
- package/dist/index.d.ts +71 -24
- package/dist/index.js +40 -288
- 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.js +63 -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 +232 -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 +16 -1
- package/dist/rich-text/rich-text-extras.d.ts +132 -0
- package/dist/rich-text/rich-text-extras.js +179 -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 +22 -4
- package/dist/router/location.js +41 -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/package.json +13 -11
|
@@ -1,103 +1,112 @@
|
|
|
1
|
+
import { isRecord } from "../guards.js";
|
|
2
|
+
// src/controller/list-params.ts
|
|
1
3
|
function isSortOrder(value) {
|
|
2
|
-
|
|
4
|
+
return value === "ASC" || value === "DESC";
|
|
3
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Serialize URL-shaped list params into a query string (with a leading `?`,
|
|
8
|
+
* or `''` when no field is set). Only defined fields are emitted; `filter` and
|
|
9
|
+
* `displayedFilters` are JSON-encoded (an empty object serializes as `{}`).
|
|
10
|
+
*/
|
|
4
11
|
function serializeListParams(params) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
if (params.order !== void 0) {
|
|
16
|
-
search.set("order", params.order);
|
|
17
|
-
}
|
|
18
|
-
if (params.page !== void 0) {
|
|
19
|
-
search.set("page", String(params.page));
|
|
20
|
-
}
|
|
21
|
-
if (params.perPage !== void 0) {
|
|
22
|
-
search.set("perPage", String(params.perPage));
|
|
23
|
-
}
|
|
24
|
-
const encoded = search.toString();
|
|
25
|
-
return encoded ? `?${encoded}` : "";
|
|
12
|
+
const search = new URLSearchParams();
|
|
13
|
+
if (params.filter !== void 0) search.set("filter", JSON.stringify(params.filter));
|
|
14
|
+
if (params.displayedFilters !== void 0) search.set("displayedFilters", JSON.stringify(params.displayedFilters));
|
|
15
|
+
if (params.sort !== void 0) search.set("sort", params.sort);
|
|
16
|
+
if (params.order !== void 0) search.set("order", params.order);
|
|
17
|
+
if (params.page !== void 0) search.set("page", String(params.page));
|
|
18
|
+
if (params.perPage !== void 0) search.set("perPage", String(params.perPage));
|
|
19
|
+
const encoded = search.toString();
|
|
20
|
+
return encoded ? `?${encoded}` : "";
|
|
26
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse a JSON-encoded object param; returns `undefined` on absence, bad JSON,
|
|
24
|
+
* or JSON that is not a plain object (`null`, an array, a primitive).
|
|
25
|
+
*/
|
|
27
26
|
function parseJsonObjectParam(raw) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
27
|
+
if (raw === null) return void 0;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (!isRecord(parsed)) return void 0;
|
|
31
|
+
return parsed;
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
37
35
|
}
|
|
36
|
+
/** Parse a positive integer param; returns `undefined` on absence, non-numeric input, or a value below 1. */
|
|
38
37
|
function parsePositiveIntParam(raw) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
if (raw === null) return void 0;
|
|
39
|
+
const parsed = Number.parseInt(raw, 10);
|
|
40
|
+
return parsed >= 1 ? parsed : void 0;
|
|
42
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse a query string (with or without a leading `?`) into URL-shaped list
|
|
44
|
+
* params. Malformed values are ignored, never trusted or thrown: bad JSON, or
|
|
45
|
+
* JSON that is not an object, on `filter`/`displayedFilters`; a non-numeric
|
|
46
|
+
* `page`/`perPage`, or one below 1; and an `order` outside `ASC`/`DESC` are
|
|
47
|
+
* simply omitted. `order` is upper-cased first.
|
|
48
|
+
*/
|
|
43
49
|
function parseListParams(search) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const page = parsePositiveIntParam(params.get("page"));
|
|
63
|
-
if (page !== void 0) result.page = page;
|
|
64
|
-
const perPage = parsePositiveIntParam(params.get("perPage"));
|
|
65
|
-
if (perPage !== void 0) result.perPage = perPage;
|
|
66
|
-
return result;
|
|
50
|
+
const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
|
|
51
|
+
const result = {};
|
|
52
|
+
const filter = parseJsonObjectParam(params.get("filter"));
|
|
53
|
+
if (filter !== void 0) result.filter = filter;
|
|
54
|
+
const displayedFilters = parseJsonObjectParam(params.get("displayedFilters"));
|
|
55
|
+
if (displayedFilters !== void 0) result.displayedFilters = displayedFilters;
|
|
56
|
+
const sort = params.get("sort");
|
|
57
|
+
if (sort !== null) result.sort = sort;
|
|
58
|
+
const rawOrder = params.get("order");
|
|
59
|
+
if (rawOrder !== null) {
|
|
60
|
+
const order = rawOrder.toUpperCase();
|
|
61
|
+
if (isSortOrder(order)) result.order = order;
|
|
62
|
+
}
|
|
63
|
+
const page = parsePositiveIntParam(params.get("page"));
|
|
64
|
+
if (page !== void 0) result.page = page;
|
|
65
|
+
const perPage = parsePositiveIntParam(params.get("perPage"));
|
|
66
|
+
if (perPage !== void 0) result.perPage = perPage;
|
|
67
|
+
return result;
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether the user has set any non-default list param — a filter with keys, an
|
|
71
|
+
* explicit sort/order, a page other than 1, or a perPage. Used by adapters to
|
|
72
|
+
* decide whether to seed the URL/store from these params or fall back to option
|
|
73
|
+
* defaults. Mirrors react-admin's `hasCustomParams` semantics (an empty filter
|
|
74
|
+
* and `page === 1` are not "custom"), without its filter-must-be-present guard.
|
|
75
|
+
*/
|
|
68
76
|
function hasCustomParams(params) {
|
|
69
|
-
|
|
77
|
+
return params.filter !== void 0 && Object.keys(params.filter).length > 0 || params.sort !== void 0 || params.order !== void 0 || params.page !== void 0 && params.page !== 1 || params.perPage !== void 0;
|
|
70
78
|
}
|
|
79
|
+
/** The query string for a list's state; the primary sort becomes `sort`/`order`. */
|
|
71
80
|
function serializeListState(state) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
const primary = state.sort[0];
|
|
82
|
+
return serializeListParams({
|
|
83
|
+
filter: state.filter,
|
|
84
|
+
displayedFilters: state.displayedFilters,
|
|
85
|
+
sort: primary?.field,
|
|
86
|
+
order: primary?.order,
|
|
87
|
+
page: state.page,
|
|
88
|
+
perPage: state.perPage
|
|
89
|
+
});
|
|
81
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The params a URL query string sets, as one `setParams` patch, or `null` when
|
|
93
|
+
* it sets none of {@link hasCustomParams}, so a clean URL keeps the option and
|
|
94
|
+
* store defaults. Shown filter inputs without a filter clear the filter.
|
|
95
|
+
*/
|
|
82
96
|
function listPatchFromSearch(search) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
const query = parseListParams(search);
|
|
98
|
+
if (!hasCustomParams(query)) return null;
|
|
99
|
+
const patch = {};
|
|
100
|
+
if (query.filter !== void 0 || query.displayedFilters !== void 0) {
|
|
101
|
+
patch.filter = query.filter ?? {};
|
|
102
|
+
patch.displayedFilters = query.displayedFilters;
|
|
103
|
+
}
|
|
104
|
+
if (query.sort !== void 0) patch.sort = {
|
|
105
|
+
field: query.sort,
|
|
106
|
+
order: query.order ?? "ASC"
|
|
107
|
+
};
|
|
108
|
+
if (query.perPage !== void 0) patch.perPage = query.perPage;
|
|
109
|
+
if (query.page !== void 0) patch.page = query.page;
|
|
110
|
+
return patch;
|
|
96
111
|
}
|
|
97
|
-
export {
|
|
98
|
-
hasCustomParams,
|
|
99
|
-
listPatchFromSearch,
|
|
100
|
-
parseListParams,
|
|
101
|
-
serializeListParams,
|
|
102
|
-
serializeListState
|
|
103
|
-
};
|
|
112
|
+
export { hasCustomParams, listPatchFromSearch, parseListParams, serializeListParams, serializeListState };
|
|
@@ -1,105 +1,99 @@
|
|
|
1
1
|
import { getAtPath } from "../resource/get-at-path.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
// src/controller/pivot-grid.ts
|
|
3
|
+
/**
|
|
4
|
+
* Pure rule for pivoting a flat record list into a row by column table. No
|
|
5
|
+
* react, no vue: both PivotGrid adapters build the same table from the same
|
|
6
|
+
* records and print numbers the same way.
|
|
7
|
+
*/
|
|
8
|
+
var collator = new Intl.Collator(void 0, { numeric: true });
|
|
9
|
+
var NUMERIC_KEY = /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i;
|
|
10
|
+
var compareKeys = (a, b) => {
|
|
11
|
+
if (a === b) return 0;
|
|
12
|
+
if (a === "") return 1;
|
|
13
|
+
if (b === "") return -1;
|
|
14
|
+
if (NUMERIC_KEY.test(a) && NUMERIC_KEY.test(b)) {
|
|
15
|
+
const difference = Number(a) - Number(b);
|
|
16
|
+
if (difference !== 0 && !Number.isNaN(difference)) return difference;
|
|
17
|
+
}
|
|
18
|
+
return collator.compare(a, b) || (a < b ? -1 : 1);
|
|
13
19
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
return void 0;
|
|
20
|
+
var toKey = (value) => value == null || value === "" ? "" : String(value);
|
|
21
|
+
var toNumber = (value) => {
|
|
22
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
23
|
+
if (typeof value === "string" && value !== "") {
|
|
24
|
+
const parsed = Number(value);
|
|
25
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
26
|
+
}
|
|
23
27
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return values.reduce((a, b) => b < a ? b : a);
|
|
36
|
-
case "max":
|
|
37
|
-
return values.reduce((a, b) => b > a ? b : a);
|
|
38
|
-
}
|
|
28
|
+
var aggregate = (group, aggregator) => {
|
|
29
|
+
if (group.count === 0) return null;
|
|
30
|
+
if (aggregator === "count") return group.count;
|
|
31
|
+
const { values } = group;
|
|
32
|
+
if (values.length === 0) return null;
|
|
33
|
+
switch (aggregator) {
|
|
34
|
+
case "sum": return values.reduce((total, value) => total + value, 0);
|
|
35
|
+
case "avg": return values.reduce((total, value) => total + value, 0) / values.length;
|
|
36
|
+
case "min": return values.reduce((a, b) => b < a ? b : a);
|
|
37
|
+
case "max": return values.reduce((a, b) => b > a ? b : a);
|
|
38
|
+
}
|
|
39
39
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
var newGroup = () => ({
|
|
41
|
+
count: 0,
|
|
42
|
+
values: []
|
|
43
|
+
});
|
|
44
|
+
var getOrCreate = (map, key, create) => {
|
|
45
|
+
const existing = map.get(key);
|
|
46
|
+
if (existing !== void 0) return existing;
|
|
47
|
+
const created = create();
|
|
48
|
+
map.set(key, created);
|
|
49
|
+
return created;
|
|
47
50
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
var add = (group, value) => {
|
|
52
|
+
group.count += 1;
|
|
53
|
+
if (value !== void 0) group.values.push(value);
|
|
51
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* A row or column key is `String(value)`, and a null, undefined or empty value
|
|
57
|
+
* is the key `""`. Keys sort numerically (`10` after `9`) with `""` last.
|
|
58
|
+
* `count` counts records and ignores `valueSource`. The other aggregators use
|
|
59
|
+
* finite numbers and numeric strings, and skip anything else. A cell with no
|
|
60
|
+
* records is `null`, and so is a cell with no usable value, unless it counts.
|
|
61
|
+
* A total aggregates the raw values of its row, column or table, never the
|
|
62
|
+
* cell results, so an average total is not a sum of averages.
|
|
63
|
+
*/
|
|
52
64
|
function buildPivotTable(records, options) {
|
|
53
|
-
|
|
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
|
-
columnKeys,
|
|
83
|
-
cells: rowKeys.map(
|
|
84
|
-
(row) => columnKeys.map(
|
|
85
|
-
(column) => aggregate(cellGroups.get(row)?.get(column) ?? empty, aggregator)
|
|
86
|
-
)
|
|
87
|
-
),
|
|
88
|
-
rowTotals: rowKeys.map(
|
|
89
|
-
(row) => aggregate(rowGroups.get(row), aggregator)
|
|
90
|
-
),
|
|
91
|
-
columnTotals: columnKeys.map(
|
|
92
|
-
(column) => aggregate(columnGroups.get(column), aggregator)
|
|
93
|
-
),
|
|
94
|
-
grandTotal: aggregate(tableGroup, aggregator)
|
|
95
|
-
};
|
|
65
|
+
const { rowSource, columnSource, valueSource, aggregator = "count" } = options;
|
|
66
|
+
const readsValue = aggregator !== "count" && valueSource !== void 0;
|
|
67
|
+
const cellGroups = /* @__PURE__ */ new Map();
|
|
68
|
+
const rowGroups = /* @__PURE__ */ new Map();
|
|
69
|
+
const columnGroups = /* @__PURE__ */ new Map();
|
|
70
|
+
const tableGroup = newGroup();
|
|
71
|
+
for (const record of records) {
|
|
72
|
+
const row = toKey(getAtPath(record, rowSource));
|
|
73
|
+
const column = toKey(getAtPath(record, columnSource));
|
|
74
|
+
const value = readsValue ? toNumber(getAtPath(record, valueSource)) : void 0;
|
|
75
|
+
const cell = getOrCreate(getOrCreate(cellGroups, row, () => /* @__PURE__ */ new Map()), column, newGroup);
|
|
76
|
+
const rowGroup = getOrCreate(rowGroups, row, newGroup);
|
|
77
|
+
const columnGroup = getOrCreate(columnGroups, column, newGroup);
|
|
78
|
+
add(cell, value);
|
|
79
|
+
add(rowGroup, value);
|
|
80
|
+
add(columnGroup, value);
|
|
81
|
+
add(tableGroup, value);
|
|
82
|
+
}
|
|
83
|
+
const rowKeys = [...rowGroups.keys()].sort(compareKeys);
|
|
84
|
+
const columnKeys = [...columnGroups.keys()].sort(compareKeys);
|
|
85
|
+
const empty = newGroup();
|
|
86
|
+
return {
|
|
87
|
+
rowKeys,
|
|
88
|
+
columnKeys,
|
|
89
|
+
cells: rowKeys.map((row) => columnKeys.map((column) => aggregate(cellGroups.get(row)?.get(column) ?? empty, aggregator))),
|
|
90
|
+
rowTotals: rowKeys.map((row) => aggregate(rowGroups.get(row), aggregator)),
|
|
91
|
+
columnTotals: columnKeys.map((column) => aggregate(columnGroups.get(column), aggregator)),
|
|
92
|
+
grandTotal: aggregate(tableGroup, aggregator)
|
|
93
|
+
};
|
|
96
94
|
}
|
|
95
|
+
/** Integers print bare and fractions round to two digits. */
|
|
97
96
|
function formatPivotValue(value, locale) {
|
|
98
|
-
|
|
99
|
-
value
|
|
100
|
-
);
|
|
97
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(value);
|
|
101
98
|
}
|
|
102
|
-
export {
|
|
103
|
-
buildPivotTable,
|
|
104
|
-
formatPivotValue
|
|
105
|
-
};
|
|
99
|
+
export { buildPivotTable, formatPivotValue };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* and the cached page, then asks these functions what to fetch and who the
|
|
5
5
|
* neighbours are.
|
|
6
6
|
*/
|
|
7
|
-
import type { FilterPayload, GetListParams, GetListResult, SortPayload } from "../data/types.js";
|
|
7
|
+
import type { FilterPayload, GetListParams, GetListResult, InfiniteListParams, SortPayload } from "../data/types.js";
|
|
8
8
|
import type { Identifier } from "../resource/identifier.js";
|
|
9
9
|
/** The list's persisted params, read from the `<resource>.listParams` store key. */
|
|
10
10
|
export interface PrevNextStoredParams {
|
|
@@ -31,7 +31,7 @@ export interface PrevNextOptions {
|
|
|
31
31
|
}
|
|
32
32
|
export interface PrevNextParams {
|
|
33
33
|
/** The page the list itself showed: the key its query cached it under. */
|
|
34
|
-
cached:
|
|
34
|
+
cached: InfiniteListParams;
|
|
35
35
|
/** The first `limit` rows, for when the cached page cannot serve. */
|
|
36
36
|
fetch: GetListParams;
|
|
37
37
|
}
|
|
@@ -1,62 +1,82 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// src/controller/prev-next.ts
|
|
2
|
+
var DEFAULT_PREV_NEXT_LIMIT = 1e3;
|
|
3
|
+
var DEFAULT_PER_PAGE = 10;
|
|
4
|
+
var DEFAULT_SORT = {
|
|
5
|
+
field: "id",
|
|
6
|
+
order: "ASC"
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A list keys its query with a single sort object when there is exactly one
|
|
10
|
+
* sort key, so the cached page is found only under that shape.
|
|
11
|
+
*/
|
|
4
12
|
function toListSort(sort) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
if (Array.isArray(sort)) {
|
|
14
|
+
const [only] = sort;
|
|
15
|
+
return sort.length === 1 && only ? { ...only } : sort;
|
|
16
|
+
}
|
|
17
|
+
return sort ?? { ...DEFAULT_SORT };
|
|
9
18
|
}
|
|
10
19
|
function getPrevNextParams(stored, options = {}) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
const persisted = options.storeKey === false ? void 0 : stored;
|
|
21
|
+
const page = persisted?.page ?? 1;
|
|
22
|
+
const perPage = persisted?.perPage ?? DEFAULT_PER_PAGE;
|
|
23
|
+
const sort = toListSort(persisted?.sort && (!Array.isArray(persisted.sort) || persisted.sort.length) ? persisted.sort : options.sort);
|
|
24
|
+
const filter = {
|
|
25
|
+
...persisted?.filter ?? options.filterDefaultValues,
|
|
26
|
+
...options.filter
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
cached: {
|
|
30
|
+
pagination: {
|
|
31
|
+
page,
|
|
32
|
+
perPage
|
|
33
|
+
},
|
|
34
|
+
sort,
|
|
35
|
+
filter
|
|
36
|
+
},
|
|
37
|
+
fetch: {
|
|
38
|
+
pagination: {
|
|
39
|
+
page: 1,
|
|
40
|
+
perPage: options.limit ?? 1e3
|
|
41
|
+
},
|
|
42
|
+
sort,
|
|
43
|
+
filter,
|
|
44
|
+
meta: options.meta
|
|
45
|
+
}
|
|
46
|
+
};
|
|
33
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether the page a list cached holds both neighbours of `id`. The first row
|
|
50
|
+
* of a later page has its previous record on the page before, and the last row
|
|
51
|
+
* of an earlier page has its next one on the page after.
|
|
52
|
+
*/
|
|
34
53
|
function canUseCachedPage(id, cached, page, perPage) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return true;
|
|
54
|
+
if (!cached) return false;
|
|
55
|
+
const index = cached.data.findIndex((record) => record.id === id);
|
|
56
|
+
if (index === -1) return false;
|
|
57
|
+
if (index === 0 && page > 1) return false;
|
|
58
|
+
if (index === cached.data.length - 1) {
|
|
59
|
+
if (cached.total != null ? page * perPage < cached.total : cached.pageInfo?.hasNextPage ?? false) return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
44
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The neighbours of `id` among `records`. `offset` is the number of rows that
|
|
65
|
+
* precede `records` in the whole list, so `index` is a position in that list.
|
|
66
|
+
*/
|
|
45
67
|
function resolvePrevNext(id, records, total, offset) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
const position = records.findIndex((record) => record.id === id);
|
|
69
|
+
if (position === -1) return {
|
|
70
|
+
prevId: null,
|
|
71
|
+
nextId: null,
|
|
72
|
+
index: void 0,
|
|
73
|
+
total
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
prevId: records[position - 1]?.id ?? null,
|
|
77
|
+
nextId: records[position + 1]?.id ?? null,
|
|
78
|
+
index: position + offset,
|
|
79
|
+
total
|
|
80
|
+
};
|
|
56
81
|
}
|
|
57
|
-
export {
|
|
58
|
-
DEFAULT_PREV_NEXT_LIMIT,
|
|
59
|
-
canUseCachedPage,
|
|
60
|
-
getPrevNextParams,
|
|
61
|
-
resolvePrevNext
|
|
62
|
-
};
|
|
82
|
+
export { DEFAULT_PREV_NEXT_LIMIT, canUseCachedPage, getPrevNextParams, resolvePrevNext };
|
|
@@ -2,6 +2,7 @@ import { type MutationMode } from "../data/mutation-options.js";
|
|
|
2
2
|
import { type DataContext } from "../data/query-options.js";
|
|
3
3
|
import type { NotificationStore } from "../notification/notification-store.js";
|
|
4
4
|
import type { Identifier } from "../resource/identifier.js";
|
|
5
|
+
import type { ResourceRecord } from "../resource/record.js";
|
|
5
6
|
import type { RecordOf, ResourceMap } from "../resource/registry.js";
|
|
6
7
|
import { type MicroStore } from "../store/micro-store.js";
|
|
7
8
|
import type { CheloneStore } from "../store/store.js";
|
|
@@ -9,7 +10,7 @@ export interface ShowControllerState<TRecord> {
|
|
|
9
10
|
record: TRecord | undefined;
|
|
10
11
|
isPending: boolean;
|
|
11
12
|
isFetching: boolean;
|
|
12
|
-
error:
|
|
13
|
+
error: Error | null;
|
|
13
14
|
}
|
|
14
15
|
export interface EditControllerState<TRecord> extends ShowControllerState<TRecord> {
|
|
15
16
|
saving: boolean;
|
|
@@ -19,7 +20,7 @@ export type RedirectTarget = "list" | "edit" | "show" | "create";
|
|
|
19
20
|
* Controllers NEVER touch a router or window — navigation intent surfaces as
|
|
20
21
|
* this callback and the adapter (or app) decides what to do with it.
|
|
21
22
|
*/
|
|
22
|
-
export type RedirectRequest = (target: RedirectTarget, resource: string, id?: Identifier, data?:
|
|
23
|
+
export type RedirectRequest = (target: RedirectTarget, resource: string, id?: Identifier, data?: ResourceRecord) => void;
|
|
23
24
|
export interface RecordControllerContext<TResources extends ResourceMap = ResourceMap> extends DataContext<TResources> {
|
|
24
25
|
notifications?: NotificationStore;
|
|
25
26
|
translate?: (key: string) => string;
|
|
@@ -39,17 +40,24 @@ export type SaveTransform<TRecord> = (data: Partial<TRecord>, extra?: {
|
|
|
39
40
|
previousData?: TRecord;
|
|
40
41
|
}) => Partial<TRecord> | Promise<Partial<TRecord>>;
|
|
41
42
|
export interface SaveOptions<TRecord> {
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Call-time transform — takes priority over the controller-level one.
|
|
45
|
+
* Declared as a method (like `onSaved` and `onSuccess`) so `SaveOptions<Post>`
|
|
46
|
+
* stays assignable to `SaveOptions<ResourceRecord>`: a view publishes its
|
|
47
|
+
* `save` into a context typed for the default record.
|
|
48
|
+
*/
|
|
49
|
+
transform?(data: Partial<TRecord>, extra?: {
|
|
50
|
+
previousData?: TRecord;
|
|
51
|
+
}): Partial<TRecord> | Promise<Partial<TRecord>>;
|
|
44
52
|
/**
|
|
45
53
|
* OBSERVES the saved record WITHOUT replacing the default success handling
|
|
46
54
|
* (runs before `onSuccess`/defaults). The P5 composed form controllers
|
|
47
55
|
* reconcile form state with server truth through this — the save contract
|
|
48
56
|
* itself resolves `FieldErrors | undefined`, never the record.
|
|
49
57
|
*/
|
|
50
|
-
onSaved
|
|
58
|
+
onSaved?(record: TRecord): void | Promise<void>;
|
|
51
59
|
/** REPLACES the default success handling (notify + redirect request). */
|
|
52
|
-
onSuccess
|
|
60
|
+
onSuccess?(record: TRecord): void | Promise<void>;
|
|
53
61
|
/** REPLACES the default error handling (the http_error notify). */
|
|
54
62
|
onError?: (error: unknown) => void | Promise<void>;
|
|
55
63
|
meta?: unknown;
|