@chelone/core 0.10.0 → 0.11.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/block-editor/block-document.d.ts +1 -1
- package/dist/controller/calendar-events.d.ts +6 -4
- package/dist/controller/calendar-events.js +2 -3
- package/dist/controller/client-list-controller.d.ts +3 -3
- package/dist/controller/client-list-controller.js +4 -20
- package/dist/controller/filter-toggle.d.ts +4 -4
- package/dist/controller/filter-toggle.js +0 -7
- package/dist/controller/in-place-editor.d.ts +1 -1
- package/dist/controller/infinite-list-controller.d.ts +1 -1
- package/dist/controller/kanban-board.d.ts +5 -3
- package/dist/controller/list-controller.d.ts +105 -28
- package/dist/controller/list-controller.js +85 -43
- package/dist/controller/list-params.d.ts +16 -13
- package/dist/controller/record-diff.d.ts +4 -3
- package/dist/controller/record-diff.js +2 -1
- package/dist/controller/reference-many-controller.d.ts +9 -7
- package/dist/controller/reference-one-controller.d.ts +8 -6
- package/dist/controller/saved-queries.d.ts +15 -14
- package/dist/controller/saved-queries.js +5 -0
- package/dist/csv/import-controller.d.ts +2 -1
- package/dist/csv/types.d.ts +12 -5
- package/dist/data/cache-ops.d.ts +8 -5
- package/dist/data/remove-empty.d.ts +2 -1
- package/dist/data/remove-empty.js +3 -5
- package/dist/data/types.d.ts +109 -6
- package/dist/data/undoable.d.ts +3 -2
- package/dist/form/choices-controller.d.ts +19 -12
- package/dist/form/choices-controller.js +19 -7
- package/dist/form/filter-form-values.d.ts +4 -3
- package/dist/form/filter-form-values.js +7 -5
- package/dist/i18n/direction.d.ts +18 -0
- package/dist/i18n/direction.js +72 -0
- package/dist/i18n/english-messages.d.ts +145 -150
- package/dist/i18n/english-messages.js +145 -150
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/realtime/live.d.ts +5 -5
- package/dist/realtime/types.d.ts +6 -6
- package/dist/resource/describe-resource.d.ts +9 -3
- package/dist/resource/registry-store.d.ts +5 -2
- package/dist/resource/source-path.d.ts +23 -0
- package/package.json +1 -1
|
@@ -45,7 +45,7 @@ export declare function getBlockDefaultAttrs<A extends Attrs>(schema: BlockSchem
|
|
|
45
45
|
[K in keyof A]: A[K] | null;
|
|
46
46
|
};
|
|
47
47
|
/** One descriptor per attr, for an adapter to build the block's config form from. */
|
|
48
|
-
export declare function describeBlockAttrs(schema: BlockSchema<Attrs>): FieldDescriptor[];
|
|
48
|
+
export declare function describeBlockAttrs(schema: BlockSchema<Attrs>): FieldDescriptor<string>[];
|
|
49
49
|
export declare function matchBlocks<B extends BlockSpec>(blocks: readonly B[], query: string): B[];
|
|
50
50
|
/**
|
|
51
51
|
* Wraps each node of an unknown type in an `unknownBlock`, so the editor keeps
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FilterPayload } from "../data/types.js";
|
|
2
|
+
import type { LooseRecord } from "../resource/record.js";
|
|
3
|
+
import type { FilterSourceKey, SourcePath } from "../resource/source-path.js";
|
|
2
4
|
import { type CalendarRange } from "./calendar-dates.js";
|
|
3
5
|
export interface CalendarEvent<R> {
|
|
4
6
|
record: R;
|
|
@@ -45,11 +47,11 @@ export declare function moveCalendarEvent<R>(event: CalendarEvent<R>, fromDay: D
|
|
|
45
47
|
* began earlier and runs into the range is fetched; a record with an empty end
|
|
46
48
|
* is then left out. Without one, only the start is bounded, on both sides.
|
|
47
49
|
*/
|
|
48
|
-
export declare function calendarRangeFilter(startSource:
|
|
50
|
+
export declare function calendarRangeFilter<R = LooseRecord>(startSource: SourcePath<R>, range: CalendarRange, endSource?: SourcePath<R>): FilterPayload<R>;
|
|
49
51
|
/**
|
|
50
52
|
* Keys an earlier default filter wrote that the current one no longer does: a
|
|
51
53
|
* list URL or store can still hold the start's lower bound, which would hide
|
|
52
54
|
* the event that starts before the range.
|
|
53
55
|
*/
|
|
54
|
-
export declare function staleCalendarRangeKeys(startSource:
|
|
55
|
-
export declare function omitCalendarRangeFilter(filter:
|
|
56
|
+
export declare function staleCalendarRangeKeys<R = LooseRecord>(startSource: SourcePath<R>, endSource?: SourcePath<R>): FilterSourceKey<R>[];
|
|
57
|
+
export declare function omitCalendarRangeFilter<R = LooseRecord>(filter: FilterPayload<R>, startSource: SourcePath<R>, endSource?: SourcePath<R>): FilterPayload<R>;
|
|
@@ -84,11 +84,10 @@ function moveCalendarEvent(event, fromDay, toDay) {
|
|
|
84
84
|
* is then left out. Without one, only the start is bounded, on both sides.
|
|
85
85
|
*/
|
|
86
86
|
function calendarRangeFilter(startSource, range, endSource) {
|
|
87
|
-
|
|
87
|
+
return endSource ? {
|
|
88
88
|
[`${startSource}_lte`]: range.end.toISOString(),
|
|
89
89
|
[`${endSource}_gte`]: range.start.toISOString()
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
90
|
+
} : {
|
|
92
91
|
[`${startSource}_gte`]: range.start.toISOString(),
|
|
93
92
|
[`${startSource}_lte`]: range.end.toISOString()
|
|
94
93
|
};
|
|
@@ -11,9 +11,9 @@ export interface ClientListControllerOptions<TRecord> {
|
|
|
11
11
|
/** The in-memory records. `undefined` = not-yet-loaded (data/total stay undefined). */
|
|
12
12
|
data?: TRecord[];
|
|
13
13
|
/** Initial user filter. */
|
|
14
|
-
filter?: FilterPayload
|
|
14
|
+
filter?: FilterPayload<TRecord>;
|
|
15
15
|
/** Initial sort. Single payload or an array (multi-sort is normalized to an array). */
|
|
16
|
-
sort?: SortPayload | SortPayload[];
|
|
16
|
+
sort?: SortPayload<TRecord> | SortPayload<TRecord>[];
|
|
17
17
|
/** Initial page (1-based). Default 1. */
|
|
18
18
|
page?: number;
|
|
19
19
|
/** Page size. Default 1000 (ra-core `useList` default — "show everything"). */
|
|
@@ -45,7 +45,7 @@ export interface ClientListController<TRecord> extends ListController<TRecord> {
|
|
|
45
45
|
/** Replace the source records (external `data` prop change) and recompute. */
|
|
46
46
|
setData(data: TRecord[] | undefined): void;
|
|
47
47
|
/** Replace the user filter from the `filter` prop (external change) and recompute. */
|
|
48
|
-
setSourceFilter(filter: FilterPayload): void;
|
|
48
|
+
setSourceFilter(filter: FilterPayload<TRecord>): void;
|
|
49
49
|
/**
|
|
50
50
|
* Push external loading-flag changes (async source feeding a client list).
|
|
51
51
|
* An `isPending` transition re-runs the derived-slice recompute so data
|
|
@@ -2,14 +2,10 @@ import { isFilterBranch } from "../data/filter-branch.js";
|
|
|
2
2
|
import { removeEmpty } from "../data/remove-empty.js";
|
|
3
3
|
import { getAtPath } from "../resource/get-at-path.js";
|
|
4
4
|
import { createMicroStore } from "../store/micro-store.js";
|
|
5
|
-
import { listParamsFromPatch, removeFilterPath } from "./list-controller.js";
|
|
5
|
+
import { listParamsFromPatch, nextSortOf, normalizeSort, removeDisplayedFilter, removeFilterPath } from "./list-controller.js";
|
|
6
6
|
// src/controller/client-list-controller.ts
|
|
7
7
|
var DEFAULT_PER_PAGE = 1e3;
|
|
8
8
|
var DEFAULT_SORT = [];
|
|
9
|
-
function normalizeSort(sort) {
|
|
10
|
-
if (!sort) return DEFAULT_SORT.map((entry) => ({ ...entry }));
|
|
11
|
-
return (Array.isArray(sort) ? sort : [sort]).map((entry) => ({ ...entry }));
|
|
12
|
-
}
|
|
13
9
|
/**
|
|
14
10
|
* Flatten a nested filter object into dotted keys (`{nested:{level:5}}` →
|
|
15
11
|
* `{'nested.level':5}`), matching ra-core's `flattenObject`. Arrays and
|
|
@@ -75,7 +71,7 @@ function createClientListController(options) {
|
|
|
75
71
|
const store = createMicroStore({
|
|
76
72
|
page: options.page ?? 1,
|
|
77
73
|
perPage: options.perPage ?? DEFAULT_PER_PAGE,
|
|
78
|
-
sort: normalizeSort(options.sort),
|
|
74
|
+
sort: normalizeSort(options.sort, DEFAULT_SORT),
|
|
79
75
|
filter: removeEmpty(options.filter ?? {}),
|
|
80
76
|
displayedFilters: {},
|
|
81
77
|
selectedIds: [],
|
|
@@ -134,20 +130,8 @@ function createClientListController(options) {
|
|
|
134
130
|
};
|
|
135
131
|
const setSort = (input) => {
|
|
136
132
|
if (destroyed) return;
|
|
137
|
-
const current = store.getSnapshot().sort;
|
|
138
|
-
const single = current.length === 1 ? current[0] : void 0;
|
|
139
|
-
let next;
|
|
140
|
-
if (Array.isArray(input)) next = input.map((entry) => ({ ...entry }));
|
|
141
|
-
else if (single && single.field === input.field) next = [{
|
|
142
|
-
field: input.field,
|
|
143
|
-
order: single.order === "ASC" ? "DESC" : "ASC"
|
|
144
|
-
}];
|
|
145
|
-
else next = [{
|
|
146
|
-
field: input.field,
|
|
147
|
-
order: input.order ?? "ASC"
|
|
148
|
-
}];
|
|
149
133
|
store.setState({
|
|
150
|
-
sort:
|
|
134
|
+
sort: nextSortOf(store.getSnapshot().sort, input),
|
|
151
135
|
page: 1
|
|
152
136
|
});
|
|
153
137
|
recompute();
|
|
@@ -187,7 +171,7 @@ function createClientListController(options) {
|
|
|
187
171
|
if (destroyed) return;
|
|
188
172
|
const s = store.getSnapshot();
|
|
189
173
|
const filter = removeFilterPath(s.filter, name);
|
|
190
|
-
const
|
|
174
|
+
const displayedFilters = removeDisplayedFilter(s.displayedFilters, name);
|
|
191
175
|
store.setState({
|
|
192
176
|
filter,
|
|
193
177
|
displayedFilters,
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* Chelone divergence: arrays compare for deep equality, not lodash's
|
|
6
6
|
* contains-all rule.
|
|
7
7
|
*/
|
|
8
|
-
type
|
|
8
|
+
import type { FilterPayload } from "../data/types.js";
|
|
9
|
+
import type { LooseRecord } from "../resource/record.js";
|
|
9
10
|
/** True when every defined key of `value` is held by `filters`. */
|
|
10
|
-
export declare function isFilterSelected(value:
|
|
11
|
+
export declare function isFilterSelected<TRecord = LooseRecord>(value: FilterPayload<TRecord>, filters: FilterPayload<TRecord>): boolean;
|
|
11
12
|
/**
|
|
12
13
|
* Selecting a value merges it into the filters; toggling a selected one drops
|
|
13
14
|
* the keys it holds, down to nested sub-keys, and keeps the rest. Neither
|
|
14
15
|
* argument is mutated.
|
|
15
16
|
*/
|
|
16
|
-
export declare function toggleFilterValue(value:
|
|
17
|
-
export {};
|
|
17
|
+
export declare function toggleFilterValue<TRecord = LooseRecord>(value: FilterPayload<TRecord>, filters: FilterPayload<TRecord>): FilterPayload<TRecord>;
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { isPlainObject } from "../guards.js";
|
|
2
2
|
import { deepEqual } from "./deep-equal.js";
|
|
3
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
|
-
*/
|
|
11
4
|
function matches(expected, actual) {
|
|
12
5
|
if (isPlainObject(expected)) return isPlainObject(actual) && Object.keys(expected).every((key) => expected[key] === void 0 || matches(expected[key], actual[key]));
|
|
13
6
|
return deepEqual(expected, actual);
|
|
@@ -40,4 +40,4 @@ export interface InPlaceSave<R extends ResourceRecord> {
|
|
|
40
40
|
* then typed back is dropped. Returns `null` when nothing changed, so a save
|
|
41
41
|
* with no change makes no call.
|
|
42
42
|
*/
|
|
43
|
-
export declare function planInPlaceSave<R extends ResourceRecord>(record: R, dirty:
|
|
43
|
+
export declare function planInPlaceSave<R extends ResourceRecord>(record: R, dirty: Partial<NoInfer<R>>): InPlaceSave<R> | null;
|
|
@@ -28,4 +28,4 @@ export interface InfiniteListController<TRecord> extends MicroStore<InfiniteList
|
|
|
28
28
|
* out-of-range page reset of the list controller does not apply: the inner
|
|
29
29
|
* controller reads a private empty cache, so no result can trigger it.
|
|
30
30
|
*/
|
|
31
|
-
export declare function createInfiniteListController<TResources extends ResourceMap, TName extends keyof TResources & string>(ctx: ListControllerContext<TResources>, resource: TName, options?: ListControllerOptions): InfiniteListController<RecordOf<TResources, TName>>;
|
|
31
|
+
export declare function createInfiniteListController<TResources extends ResourceMap, TName extends keyof TResources & string>(ctx: ListControllerContext<TResources>, resource: TName, options?: ListControllerOptions<RecordOf<TResources, TName>>): InfiniteListController<RecordOf<TResources, TName>>;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* beside {@link moveCalendarEvent}.
|
|
9
9
|
*/
|
|
10
10
|
import type { Identifier } from "../resource/identifier.js";
|
|
11
|
-
import type { ResourceRecord } from "../resource/record.js";
|
|
11
|
+
import type { LooseRecord, ResourceRecord } from "../resource/record.js";
|
|
12
12
|
import type { SourceKey } from "../resource/source-path.js";
|
|
13
13
|
export interface KanbanColumnDef {
|
|
14
14
|
/** Value written to `groupSource` by a drop into this column. */
|
|
@@ -25,9 +25,11 @@ export interface KanbanBucket<R> {
|
|
|
25
25
|
* and `previousData` the record as it was, so an adapter can undo the drop
|
|
26
26
|
* without a second read.
|
|
27
27
|
*/
|
|
28
|
-
export interface KanbanMove<R> {
|
|
28
|
+
export interface KanbanMove<R = LooseRecord> {
|
|
29
29
|
id: Identifier;
|
|
30
|
-
data:
|
|
30
|
+
data: {
|
|
31
|
+
[K in SourceKey<R>]?: string;
|
|
32
|
+
};
|
|
31
33
|
previousData: R;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { type DataContext } from "../data/query-options.js";
|
|
2
|
-
import type { FilterPayload, PageInfo, SortPayload } from "../data/types.js";
|
|
2
|
+
import type { DisplayedFilters, FilterPayload, LooseDisplayedFilters, LooseFilterPayload, LooseSortPayload, PageInfo, SortPayload } from "../data/types.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
|
+
import type { LooseRecord, ResourceRecord } from "../resource/record.js";
|
|
6
6
|
import type { RecordOf, ResourceMap } from "../resource/registry.js";
|
|
7
|
+
import type { RecordFilterKey } from "../resource/source-path.js";
|
|
7
8
|
import { type MicroStore } from "../store/micro-store.js";
|
|
8
9
|
import type { CheloneStore } from "../store/store.js";
|
|
9
|
-
export interface ListControllerState<TRecord> {
|
|
10
|
+
export interface ListControllerState<TRecord = LooseRecord> {
|
|
10
11
|
page: number;
|
|
11
12
|
perPage: number;
|
|
12
13
|
/** Always an array in controller state — multi-sort is first-class. */
|
|
13
|
-
sort: SortPayload[];
|
|
14
|
-
filter: FilterPayload
|
|
15
|
-
displayedFilters:
|
|
14
|
+
sort: SortPayload<TRecord>[];
|
|
15
|
+
filter: FilterPayload<TRecord>;
|
|
16
|
+
displayedFilters: DisplayedFilters<TRecord>;
|
|
16
17
|
selectedIds: Identifier[];
|
|
17
18
|
data: TRecord[] | undefined;
|
|
18
19
|
total: number | undefined;
|
|
@@ -28,28 +29,87 @@ export interface ListControllerState<TRecord> {
|
|
|
28
29
|
* write and one query reconfiguration. Every key is optional; an omitted key
|
|
29
30
|
* stays as it is.
|
|
30
31
|
*/
|
|
31
|
-
export interface ListParamsPatch {
|
|
32
|
+
export interface ListParamsPatch<TRecord = LooseRecord> {
|
|
32
33
|
/** Replaces the user filter; empty values are stripped. */
|
|
33
|
-
filter?: FilterPayload
|
|
34
|
-
displayedFilters?:
|
|
34
|
+
filter?: FilterPayload<TRecord>;
|
|
35
|
+
displayedFilters?: DisplayedFilters<TRecord>;
|
|
35
36
|
/** Replaces wholesale; a single payload never toggles the order. */
|
|
36
|
-
sort?: SortPayload | SortPayload[];
|
|
37
|
+
sort?: SortPayload<TRecord> | SortPayload<TRecord>[];
|
|
37
38
|
perPage?: number;
|
|
38
39
|
/** Defaults to 1 when the patch holds `filter`, `sort` or `perPage`. */
|
|
39
40
|
page?: number;
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* A {@link ListParamsPatch} with the record type erased: the filter and the
|
|
44
|
+
* displayed filters are open key maps and the sort an open field name. Each
|
|
45
|
+
* adapter's list context carries this, because a context cannot be generic and
|
|
46
|
+
* a controller typed to any record must mount in one.
|
|
47
|
+
*
|
|
48
|
+
* Written as `Omit` plus fresh `filter`, `displayedFilters` and `sort`, not as
|
|
49
|
+
* `ListParamsPatch` itself:
|
|
50
|
+
* TypeScript compares two instantiations of one generic interface by the
|
|
51
|
+
* variance of their type argument, so a `ListParamsPatch<Post>` never reaches
|
|
52
|
+
* `ListParamsPatch<LooseRecord>` however the fields read. As a separate type
|
|
53
|
+
* the two line up field by field.
|
|
54
|
+
*/
|
|
55
|
+
export type LooseListParamsPatch = Omit<ListParamsPatch, "filter" | "sort" | "displayedFilters"> & {
|
|
56
|
+
filter?: LooseFilterPayload;
|
|
57
|
+
displayedFilters?: LooseDisplayedFilters;
|
|
58
|
+
sort?: LooseSortPayload | LooseSortPayload[];
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* A list controller's state and actions, without the micro-store plumbing each
|
|
62
|
+
* adapter re-binds through its own reactive seam (`useSyncExternalStore` in
|
|
63
|
+
* React, `useMicroStore` in Vue).
|
|
64
|
+
*/
|
|
65
|
+
export type ListControllerSurface<TRecord> = ListControllerState<TRecord> & Omit<ListController<TRecord>, keyof MicroStore<ListControllerState<TRecord>>>;
|
|
66
|
+
/**
|
|
67
|
+
* The list surface an adapter's list context publishes: the rows typed to
|
|
68
|
+
* `TRecord`, the filter and the displayed filters open key maps.
|
|
69
|
+
*
|
|
70
|
+
* The sort is as open as the filter, for the same reason: a column header or a
|
|
71
|
+
* URL names a field with no record to check it, so `sort` and `setSort` take
|
|
72
|
+
* the structural {@link LooseSortPayload}: a `SortPayload<Post>` never reaches
|
|
73
|
+
* `SortPayload<LooseRecord>` by variance, as with {@link LooseListParamsPatch}.
|
|
74
|
+
* A record's own sort fields are the job of
|
|
75
|
+
* {@link ListController.setSort} and the options of each controller.
|
|
76
|
+
*
|
|
77
|
+
* The filter is open on purpose. What writes a context filter names no record:
|
|
78
|
+
* a filter form's field names, a calendar's range keys, a URL query. A record's
|
|
79
|
+
* own filter keys are the job of {@link ListController.setFilters} and the
|
|
80
|
+
* hooks, which are generic over the record. Erasing the filter is what lets a
|
|
81
|
+
* controller typed to any record be mounted in one context.
|
|
82
|
+
*
|
|
83
|
+
* The displayed filters and the two actions that move them are open for the
|
|
84
|
+
* same reason, and they must be erased together with it: a filter input names
|
|
85
|
+
* its filter by a plain `source` (`FilterElementProps` in React's filter form),
|
|
86
|
+
* so a context keyed by the record would stop every filter input compiling in a
|
|
87
|
+
* registered app.
|
|
88
|
+
*/
|
|
89
|
+
export type ListContextSurface<TRecord = ResourceRecord> = Omit<ListControllerSurface<TRecord>, "filter" | "setFilters" | "setParams" | "sort" | "setSort" | "displayedFilters" | "showFilter" | "hideFilter"> & {
|
|
90
|
+
sort: LooseSortPayload[];
|
|
91
|
+
setSort(sort: LooseSortPayload | LooseSortPayload[]): void;
|
|
92
|
+
filter: LooseFilterPayload;
|
|
93
|
+
displayedFilters: LooseDisplayedFilters;
|
|
94
|
+
setFilters(filter: LooseFilterPayload, displayedFilters?: LooseDisplayedFilters, debounced?: boolean): void;
|
|
95
|
+
setParams(patch: LooseListParamsPatch): void;
|
|
96
|
+
showFilter(name: string, defaultValue?: unknown): void;
|
|
97
|
+
hideFilter(name: string): void;
|
|
98
|
+
};
|
|
41
99
|
export interface ListController<TRecord> extends MicroStore<ListControllerState<TRecord>> {
|
|
42
100
|
/** The `perPage` the list starts at when nothing is persisted or in the URL. Optional so other list controllers and fakes need not set it. */
|
|
43
101
|
readonly defaultPerPage?: number;
|
|
44
102
|
setPage(page: number): void;
|
|
45
103
|
setPerPage(perPage: number): void;
|
|
46
104
|
/** Single payload on the SAME single-sorted field toggles order; anything else replaces. */
|
|
47
|
-
setSort(sort: SortPayload | SortPayload[]): void;
|
|
48
|
-
setFilters(filter: FilterPayload
|
|
105
|
+
setSort(sort: SortPayload<TRecord> | SortPayload<TRecord>[]): void;
|
|
106
|
+
setFilters(filter: FilterPayload<TRecord>, displayedFilters?: DisplayedFilters<TRecord>, debounced?: boolean): void;
|
|
49
107
|
/** Applies a {@link ListParamsPatch} as one change (one persisted write, one query update; a fetch only if the query changes). An empty patch does nothing. Never debounced. */
|
|
50
|
-
setParams(patch: ListParamsPatch): void;
|
|
51
|
-
|
|
52
|
-
|
|
108
|
+
setParams(patch: ListParamsPatch<TRecord>): void;
|
|
109
|
+
/** Shows the filter input `name`, and writes `{ [name]: defaultValue }` into the filter when a default is given. */
|
|
110
|
+
showFilter(name: RecordFilterKey<TRecord>, defaultValue?: unknown): void;
|
|
111
|
+
/** Hides the filter input `name` and removes that path from the filter. */
|
|
112
|
+
hideFilter(name: RecordFilterKey<TRecord>): void;
|
|
53
113
|
select(ids: Identifier[]): void;
|
|
54
114
|
toggleItem(id: Identifier): void;
|
|
55
115
|
unselectAll(): void;
|
|
@@ -63,13 +123,13 @@ export interface ListControllerContext<TResources extends ResourceMap = Resource
|
|
|
63
123
|
notifications?: NotificationStore;
|
|
64
124
|
translate?: (key: string) => string;
|
|
65
125
|
}
|
|
66
|
-
export interface ListControllerOptions {
|
|
126
|
+
export interface ListControllerOptions<TRecord = LooseRecord> {
|
|
67
127
|
perPage?: number;
|
|
68
|
-
sort?: SortPayload | SortPayload[];
|
|
128
|
+
sort?: SortPayload<TRecord> | SortPayload<TRecord>[];
|
|
69
129
|
/** Permanent filter — merged OVER the user filter for every fetch. */
|
|
70
|
-
filter?: FilterPayload
|
|
130
|
+
filter?: FilterPayload<TRecord>;
|
|
71
131
|
/** Seed for the user filter when nothing is persisted. */
|
|
72
|
-
filterDefaultValues?: FilterPayload
|
|
132
|
+
filterDefaultValues?: FilterPayload<TRecord>;
|
|
73
133
|
/** Debounce delay for `setFilters(..., debounced=true)`. Default 500ms. */
|
|
74
134
|
debounceMs?: number;
|
|
75
135
|
/** KV namespace for params/selection persistence. Default: the resource name. `false` disables persistence. */
|
|
@@ -81,17 +141,34 @@ export declare const DEFAULT_PER_PAGE = 10;
|
|
|
81
141
|
/** Debounce delay for `setFilters(..., debounced=true)` when no `debounceMs` option is given. Shared with the reference-many and choices controllers. */
|
|
82
142
|
export declare const DEFAULT_DEBOUNCE_MS = 500;
|
|
83
143
|
export declare const DEFAULT_SELECT_ALL_LIMIT = 250;
|
|
144
|
+
/**
|
|
145
|
+
* A sort prop as controller state: always an array, always a fresh copy, so a
|
|
146
|
+
* caller's payload is never the object the controller holds. `fallback` is what
|
|
147
|
+
* an absent sort becomes. Shared with the client controller.
|
|
148
|
+
*/
|
|
149
|
+
export declare function normalizeSort<TRecord>(sort: SortPayload<TRecord> | SortPayload<TRecord>[] | undefined, fallback: SortPayload<TRecord>[]): SortPayload<TRecord>[];
|
|
150
|
+
/**
|
|
151
|
+
* The sort a `setSort` input produces: a single payload on the SAME
|
|
152
|
+
* single-sorted field toggles its order (ra-core column-header semantics);
|
|
153
|
+
* anything else replaces. Shared with the client controller.
|
|
154
|
+
*/
|
|
155
|
+
export declare function nextSortOf<TRecord>(current: SortPayload<TRecord>[], input: SortPayload<TRecord> | SortPayload<TRecord>[]): SortPayload<TRecord>[];
|
|
84
156
|
/**
|
|
85
157
|
* Returns `filter` without the value at `path`: the flat key of that name and,
|
|
86
158
|
* for a dotted path, the nested value, pruning parents it leaves empty. Never
|
|
87
159
|
* mutates `filter`. Shared with the client controller.
|
|
88
160
|
*/
|
|
89
|
-
export declare function removeFilterPath(filter: FilterPayload
|
|
161
|
+
export declare function removeFilterPath<TRecord = LooseRecord>(filter: FilterPayload<TRecord>, path: string): FilterPayload<TRecord>;
|
|
162
|
+
/**
|
|
163
|
+
* The displayed filters without the entry named `name`: always a fresh copy,
|
|
164
|
+
* never the map it was given. Shared with the client controller.
|
|
165
|
+
*/
|
|
166
|
+
export declare function removeDisplayedFilter<TRecord = LooseRecord>(displayedFilters: DisplayedFilters<TRecord>, name: RecordFilterKey<TRecord>): DisplayedFilters<TRecord>;
|
|
90
167
|
/**
|
|
91
168
|
* Turns a {@link ListParamsPatch} into the state slice it changes. Shared with
|
|
92
169
|
* the client controller so the page-reset rule lives in one place.
|
|
93
170
|
*/
|
|
94
|
-
export declare function listParamsFromPatch<TRecord>(patch: ListParamsPatch): Partial<Pick<ListControllerState<TRecord>, "page" | "perPage" | "sort" | "filter" | "displayedFilters">>;
|
|
171
|
+
export declare function listParamsFromPatch<TRecord>(patch: ListParamsPatch<TRecord>): Partial<Pick<ListControllerState<TRecord>, "page" | "perPage" | "sort" | "filter" | "displayedFilters">>;
|
|
95
172
|
/** The notification key for a failed fetch: the error's own message, else `chelone.notification.http_error`. */
|
|
96
173
|
export declare function errorMessageKey(error: unknown): string;
|
|
97
174
|
/** Notifies a failed fetch under its message, falling back to the generic http error. */
|
|
@@ -145,14 +222,14 @@ export declare function resetPageTarget(s: Pick<ListControllerState<ResourceReco
|
|
|
145
222
|
* The actions of a list controller, for an adapter to spread next to its own
|
|
146
223
|
* reactive state. They are the same functions across state changes.
|
|
147
224
|
*/
|
|
148
|
-
export declare function listControllerActions(controller: Omit<ListController<
|
|
225
|
+
export declare function listControllerActions<TRecord>(controller: Omit<ListController<TRecord>, keyof MicroStore<ListControllerState<TRecord>> | "defaultPerPage">): {
|
|
149
226
|
setPage: (page: number) => void;
|
|
150
227
|
setPerPage: (perPage: number) => void;
|
|
151
|
-
setSort: (sort: SortPayload | SortPayload[]) => void;
|
|
152
|
-
setFilters: (filter: FilterPayload
|
|
153
|
-
setParams: (patch: ListParamsPatch) => void;
|
|
154
|
-
showFilter: (name:
|
|
155
|
-
hideFilter: (name:
|
|
228
|
+
setSort: (sort: SortPayload<TRecord> | SortPayload<TRecord>[]) => void;
|
|
229
|
+
setFilters: (filter: FilterPayload<TRecord>, displayedFilters?: DisplayedFilters<TRecord> | undefined, debounced?: boolean) => void;
|
|
230
|
+
setParams: (patch: ListParamsPatch<TRecord>) => void;
|
|
231
|
+
showFilter: (name: RecordFilterKey<TRecord>, defaultValue?: unknown) => void;
|
|
232
|
+
hideFilter: (name: RecordFilterKey<TRecord>) => void;
|
|
156
233
|
select: (ids: Identifier[]) => void;
|
|
157
234
|
toggleItem: (id: Identifier) => void;
|
|
158
235
|
unselectAll: () => void;
|
|
@@ -161,4 +238,4 @@ export declare function listControllerActions(controller: Omit<ListController<Re
|
|
|
161
238
|
}) => Promise<void>;
|
|
162
239
|
refetch: () => void;
|
|
163
240
|
};
|
|
164
|
-
export declare function createListController<TResources extends ResourceMap, TName extends keyof TResources & string>(ctx: ListControllerContext<TResources>, resource: TName, options?: ListControllerOptions): ListController<RecordOf<TResources, TName>>;
|
|
241
|
+
export declare function createListController<TResources extends ResourceMap, TName extends keyof TResources & string>(ctx: ListControllerContext<TResources>, resource: TName, options?: ListControllerOptions<RecordOf<TResources, TName>>): ListController<RecordOf<TResources, TName>>;
|
|
@@ -8,44 +8,98 @@ import { QueryObserver } from "@tanstack/query-core";
|
|
|
8
8
|
var DEFAULT_PER_PAGE = 10;
|
|
9
9
|
/** Debounce delay for `setFilters(..., debounced=true)` when no `debounceMs` option is given. Shared with the reference-many and choices controllers. */
|
|
10
10
|
var DEFAULT_DEBOUNCE_MS = 500;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The sort a list starts on: `id` ascending, as it always was. This is the one
|
|
13
|
+
* cast this change adds. A schema is not required to declare `id`, so a record
|
|
14
|
+
* this generic cannot prove carries the path; a resource without one still sends
|
|
15
|
+
* `id` to its provider, exactly as before the sort was keyed by the record.
|
|
16
|
+
*/
|
|
17
|
+
function defaultSort() {
|
|
18
|
+
return [{
|
|
19
|
+
field: "id",
|
|
20
|
+
order: "ASC"
|
|
21
|
+
}];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A sort prop as controller state: always an array, always a fresh copy, so a
|
|
25
|
+
* caller's payload is never the object the controller holds. `fallback` is what
|
|
26
|
+
* an absent sort becomes. Shared with the client controller.
|
|
27
|
+
*/
|
|
15
28
|
function normalizeSort(sort, fallback) {
|
|
16
29
|
if (!sort) return fallback.map((entry) => ({ ...entry }));
|
|
17
30
|
return (Array.isArray(sort) ? sort : [sort]).map((entry) => ({ ...entry }));
|
|
18
31
|
}
|
|
19
32
|
/**
|
|
33
|
+
* The sort a `setSort` input produces: a single payload on the SAME
|
|
34
|
+
* single-sorted field toggles its order (ra-core column-header semantics);
|
|
35
|
+
* anything else replaces. Shared with the client controller.
|
|
36
|
+
*/
|
|
37
|
+
function nextSortOf(current, input) {
|
|
38
|
+
if (Array.isArray(input)) return input.map((entry) => ({ ...entry }));
|
|
39
|
+
const single = current.length === 1 ? current[0] : void 0;
|
|
40
|
+
if (single && single.field === input.field) return [{
|
|
41
|
+
field: input.field,
|
|
42
|
+
order: single.order === "ASC" ? "DESC" : "ASC"
|
|
43
|
+
}];
|
|
44
|
+
return [{
|
|
45
|
+
field: input.field,
|
|
46
|
+
order: input.order ?? "ASC"
|
|
47
|
+
}];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `branch` with the dotted `keys` removed from `depth` on: every branch it
|
|
51
|
+
* rebuilds is a copy, and it returns `branch` itself when nothing under it
|
|
52
|
+
* changed, so the caller can tell "untouched" from "pruned".
|
|
53
|
+
*/
|
|
54
|
+
function pruneFilterBranch(branch, keys, depth) {
|
|
55
|
+
const key = keys[depth];
|
|
56
|
+
if (key === void 0) return branch;
|
|
57
|
+
const child = branch[key];
|
|
58
|
+
if (depth === keys.length - 1) {
|
|
59
|
+
const { [key]: _removed, ...others } = branch;
|
|
60
|
+
return others;
|
|
61
|
+
}
|
|
62
|
+
if (!isRecord(child)) return branch;
|
|
63
|
+
const pruned = pruneFilterBranch(child, keys, depth + 1);
|
|
64
|
+
if (pruned === child) return branch;
|
|
65
|
+
if (Object.keys(pruned).length === 0) {
|
|
66
|
+
const { [key]: _emptied, ...others } = branch;
|
|
67
|
+
return others;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
...branch,
|
|
71
|
+
[key]: pruned
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
20
75
|
* Returns `filter` without the value at `path`: the flat key of that name and,
|
|
21
76
|
* for a dotted path, the nested value, pruning parents it leaves empty. Never
|
|
22
77
|
* mutates `filter`. Shared with the client controller.
|
|
23
78
|
*/
|
|
24
79
|
function removeFilterPath(filter, path) {
|
|
25
|
-
const
|
|
80
|
+
const result = { ...filter };
|
|
81
|
+
const pruned = result;
|
|
82
|
+
delete pruned[path];
|
|
26
83
|
const keys = path.split(".");
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
47
|
-
};
|
|
48
|
-
return prune(rest, 0);
|
|
84
|
+
const [head] = keys;
|
|
85
|
+
if (head === void 0 || keys.length === 1) return result;
|
|
86
|
+
const branch = pruned[head];
|
|
87
|
+
if (!isRecord(branch)) return result;
|
|
88
|
+
const kept = pruneFilterBranch(branch, keys, 1);
|
|
89
|
+
if (kept === branch) return result;
|
|
90
|
+
if (Object.keys(kept).length === 0) delete pruned[head];
|
|
91
|
+
else pruned[head] = kept;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The displayed filters without the entry named `name`: always a fresh copy,
|
|
96
|
+
* never the map it was given. Shared with the client controller.
|
|
97
|
+
*/
|
|
98
|
+
function removeDisplayedFilter(displayedFilters, name) {
|
|
99
|
+
const kept = { ...displayedFilters };
|
|
100
|
+
const open = kept;
|
|
101
|
+
delete open[name];
|
|
102
|
+
return kept;
|
|
49
103
|
}
|
|
50
104
|
/**
|
|
51
105
|
* Turns a {@link ListParamsPatch} into the state slice it changes. Shared with
|
|
@@ -192,7 +246,7 @@ function createListController(ctx, resource, options = {}) {
|
|
|
192
246
|
const defaults = {
|
|
193
247
|
page: 1,
|
|
194
248
|
perPage: options.perPage ?? 10,
|
|
195
|
-
sort: normalizeSort(options.sort,
|
|
249
|
+
sort: normalizeSort(options.sort, defaultSort()),
|
|
196
250
|
filter: { ...options.filterDefaultValues ?? {} },
|
|
197
251
|
displayedFilters: {}
|
|
198
252
|
};
|
|
@@ -277,20 +331,8 @@ function createListController(ctx, resource, options = {}) {
|
|
|
277
331
|
};
|
|
278
332
|
const setSort = (input) => {
|
|
279
333
|
if (destroyed) return;
|
|
280
|
-
const current = store.getSnapshot().sort;
|
|
281
|
-
const single = current.length === 1 ? current[0] : void 0;
|
|
282
|
-
let next;
|
|
283
|
-
if (Array.isArray(input)) next = input.map((entry) => ({ ...entry }));
|
|
284
|
-
else if (single && single.field === input.field) next = [{
|
|
285
|
-
field: input.field,
|
|
286
|
-
order: single.order === "ASC" ? "DESC" : "ASC"
|
|
287
|
-
}];
|
|
288
|
-
else next = [{
|
|
289
|
-
field: input.field,
|
|
290
|
-
order: input.order ?? "ASC"
|
|
291
|
-
}];
|
|
292
334
|
applyParams({
|
|
293
|
-
sort:
|
|
335
|
+
sort: nextSortOf(store.getSnapshot().sort, input),
|
|
294
336
|
page: 1
|
|
295
337
|
});
|
|
296
338
|
};
|
|
@@ -356,7 +398,7 @@ function createListController(ctx, resource, options = {}) {
|
|
|
356
398
|
const pending = pendingFilters;
|
|
357
399
|
clearDebounce();
|
|
358
400
|
const filter = removeFilterPath(pending ? removeEmpty(pending.filter) : s.filter, name);
|
|
359
|
-
const
|
|
401
|
+
const displayedFilters = removeDisplayedFilter(pending?.displayedFilters ?? s.displayedFilters, name);
|
|
360
402
|
applyParams({
|
|
361
403
|
filter,
|
|
362
404
|
displayedFilters,
|
|
@@ -469,4 +511,4 @@ function createListController(ctx, resource, options = {}) {
|
|
|
469
511
|
refetch
|
|
470
512
|
};
|
|
471
513
|
}
|
|
472
|
-
export { DEFAULT_DEBOUNCE_MS, DEFAULT_PER_PAGE, createErrorAnnouncer, createListController, errorMessageKey, listControllerActions, listParamsFromPatch, notifyFetchError, notifySelectAllLimit, pageFlags, removeFilterPath, resetPageTarget, runSelectAll };
|
|
514
|
+
export { DEFAULT_DEBOUNCE_MS, DEFAULT_PER_PAGE, createErrorAnnouncer, createListController, errorMessageKey, listControllerActions, listParamsFromPatch, nextSortOf, normalizeSort, notifyFetchError, notifySelectAllLimit, pageFlags, removeDisplayedFilter, removeFilterPath, resetPageTarget, runSelectAll };
|
|
@@ -15,17 +15,20 @@
|
|
|
15
15
|
* {@link serializeListState} and {@link listPatchFromSearch} map between the
|
|
16
16
|
* two (primary key) for the adapters' location sync.
|
|
17
17
|
*/
|
|
18
|
-
import type { FilterPayload, SortPayload } from "../data/types.js";
|
|
18
|
+
import type { DisplayedFilters, FilterPayload, SortPayload } from "../data/types.js";
|
|
19
|
+
import type { LooseRecord } from "../resource/record.js";
|
|
19
20
|
import type { ListParamsPatch } from "./list-controller.js";
|
|
20
21
|
type SortOrder = "ASC" | "DESC";
|
|
21
22
|
/**
|
|
22
23
|
* The URL-shaped list params. A flat mirror of the query string: `sort` is the
|
|
23
24
|
* field name and `order` its direction (not a `SortPayload`), so this maps 1:1
|
|
24
25
|
* onto the wire format. Every field is optional — absent means "not in the URL".
|
|
26
|
+
* `TRecord` keys `filter` and `displayedFilters`: a parse hands back what the
|
|
27
|
+
* URL names, and a list hands it to its own controller.
|
|
25
28
|
*/
|
|
26
|
-
export interface ListParamsQuery {
|
|
27
|
-
filter?: FilterPayload
|
|
28
|
-
displayedFilters?:
|
|
29
|
+
export interface ListParamsQuery<TRecord = LooseRecord> {
|
|
30
|
+
filter?: FilterPayload<TRecord>;
|
|
31
|
+
displayedFilters?: DisplayedFilters<TRecord>;
|
|
29
32
|
sort?: string;
|
|
30
33
|
order?: SortOrder;
|
|
31
34
|
page?: number;
|
|
@@ -36,7 +39,7 @@ export interface ListParamsQuery {
|
|
|
36
39
|
* or `''` when no field is set). Only defined fields are emitted; `filter` and
|
|
37
40
|
* `displayedFilters` are JSON-encoded (an empty object serializes as `{}`).
|
|
38
41
|
*/
|
|
39
|
-
export declare function serializeListParams(params: ListParamsQuery): string;
|
|
42
|
+
export declare function serializeListParams<TRecord = LooseRecord>(params: ListParamsQuery<TRecord>): string;
|
|
40
43
|
/**
|
|
41
44
|
* Parse a query string (with or without a leading `?`) into URL-shaped list
|
|
42
45
|
* params. Malformed values are ignored, never trusted or thrown: bad JSON, or
|
|
@@ -44,7 +47,7 @@ export declare function serializeListParams(params: ListParamsQuery): string;
|
|
|
44
47
|
* `page`/`perPage`, or one below 1; and an `order` outside `ASC`/`DESC` are
|
|
45
48
|
* simply omitted. `order` is upper-cased first.
|
|
46
49
|
*/
|
|
47
|
-
export declare function parseListParams(search: string): ListParamsQuery
|
|
50
|
+
export declare function parseListParams<TRecord = LooseRecord>(search: string): ListParamsQuery<TRecord>;
|
|
48
51
|
/**
|
|
49
52
|
* Whether the user has set any non-default list param — a filter with keys, an
|
|
50
53
|
* explicit sort/order, a page other than 1, or a perPage. Used by adapters to
|
|
@@ -52,21 +55,21 @@ export declare function parseListParams(search: string): ListParamsQuery;
|
|
|
52
55
|
* defaults. Mirrors react-admin's `hasCustomParams` semantics (an empty filter
|
|
53
56
|
* and `page === 1` are not "custom"), without its filter-must-be-present guard.
|
|
54
57
|
*/
|
|
55
|
-
export declare function hasCustomParams(params: ListParamsQuery): boolean;
|
|
58
|
+
export declare function hasCustomParams<TRecord = LooseRecord>(params: ListParamsQuery<TRecord>): boolean;
|
|
56
59
|
/** The list-controller state a URL carries. */
|
|
57
|
-
export interface ListUrlState {
|
|
60
|
+
export interface ListUrlState<TRecord = LooseRecord> {
|
|
58
61
|
page: number;
|
|
59
62
|
perPage: number;
|
|
60
|
-
sort: SortPayload[];
|
|
61
|
-
filter: FilterPayload
|
|
62
|
-
displayedFilters:
|
|
63
|
+
sort: SortPayload<TRecord>[];
|
|
64
|
+
filter: FilterPayload<TRecord>;
|
|
65
|
+
displayedFilters: DisplayedFilters<TRecord>;
|
|
63
66
|
}
|
|
64
67
|
/** The query string for a list's state; the primary sort becomes `sort`/`order`. */
|
|
65
|
-
export declare function serializeListState(state: ListUrlState): string;
|
|
68
|
+
export declare function serializeListState<TRecord = LooseRecord>(state: ListUrlState<TRecord>): string;
|
|
66
69
|
/**
|
|
67
70
|
* The params a URL query string sets, as one `setParams` patch, or `null` when
|
|
68
71
|
* it sets none of {@link hasCustomParams}, so a clean URL keeps the option and
|
|
69
72
|
* store defaults. Shown filter inputs without a filter clear the filter.
|
|
70
73
|
*/
|
|
71
|
-
export declare function listPatchFromSearch(search: string): ListParamsPatch | null;
|
|
74
|
+
export declare function listPatchFromSearch<TRecord = LooseRecord>(search: string): ListParamsPatch<TRecord> | null;
|
|
72
75
|
export {};
|