@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.
Files changed (42) hide show
  1. package/dist/block-editor/block-document.d.ts +1 -1
  2. package/dist/controller/calendar-events.d.ts +6 -4
  3. package/dist/controller/calendar-events.js +2 -3
  4. package/dist/controller/client-list-controller.d.ts +3 -3
  5. package/dist/controller/client-list-controller.js +4 -20
  6. package/dist/controller/filter-toggle.d.ts +4 -4
  7. package/dist/controller/filter-toggle.js +0 -7
  8. package/dist/controller/in-place-editor.d.ts +1 -1
  9. package/dist/controller/infinite-list-controller.d.ts +1 -1
  10. package/dist/controller/kanban-board.d.ts +5 -3
  11. package/dist/controller/list-controller.d.ts +105 -28
  12. package/dist/controller/list-controller.js +85 -43
  13. package/dist/controller/list-params.d.ts +16 -13
  14. package/dist/controller/record-diff.d.ts +4 -3
  15. package/dist/controller/record-diff.js +2 -1
  16. package/dist/controller/reference-many-controller.d.ts +9 -7
  17. package/dist/controller/reference-one-controller.d.ts +8 -6
  18. package/dist/controller/saved-queries.d.ts +15 -14
  19. package/dist/controller/saved-queries.js +5 -0
  20. package/dist/csv/import-controller.d.ts +2 -1
  21. package/dist/csv/types.d.ts +12 -5
  22. package/dist/data/cache-ops.d.ts +8 -5
  23. package/dist/data/remove-empty.d.ts +2 -1
  24. package/dist/data/remove-empty.js +3 -5
  25. package/dist/data/types.d.ts +109 -6
  26. package/dist/data/undoable.d.ts +3 -2
  27. package/dist/form/choices-controller.d.ts +19 -12
  28. package/dist/form/choices-controller.js +19 -7
  29. package/dist/form/filter-form-values.d.ts +4 -3
  30. package/dist/form/filter-form-values.js +7 -5
  31. package/dist/i18n/direction.d.ts +18 -0
  32. package/dist/i18n/direction.js +72 -0
  33. package/dist/i18n/english-messages.d.ts +145 -150
  34. package/dist/i18n/english-messages.js +145 -150
  35. package/dist/index.d.ts +6 -4
  36. package/dist/index.js +3 -2
  37. package/dist/realtime/live.d.ts +5 -5
  38. package/dist/realtime/types.d.ts +6 -6
  39. package/dist/resource/describe-resource.d.ts +9 -3
  40. package/dist/resource/registry-store.d.ts +5 -2
  41. package/dist/resource/source-path.d.ts +23 -0
  42. 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 { SourcePath } from "../resource/source-path.js";
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: string, range: CalendarRange, endSource?: string): Record<string, string>;
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: string, endSource?: string): string[];
55
- export declare function omitCalendarRangeFilter(filter: Record<string, unknown>, startSource: string, endSource?: string): Record<string, unknown>;
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
- if (endSource) return {
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: next,
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 { [name]: _shown, ...displayedFilters } = s.displayedFilters;
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 Filters = Record<string, unknown>;
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: Filters, filters: Filters): boolean;
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: Filters, filters: Filters): Filters;
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: Record<string, unknown>): InPlaceSave<R> | null;
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: Record<string, string>;
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: Record<string, boolean>;
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?: Record<string, boolean>;
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, displayedFilters?: Record<string, boolean>, debounced?: boolean): void;
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
- showFilter(name: string, defaultValue?: unknown): void;
52
- hideFilter(name: string): void;
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, path: string): 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<ResourceRecord>, keyof MicroStore<ListControllerState<ResourceRecord>> | "defaultPerPage">): {
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, displayedFilters?: Record<string, boolean>, debounced?: boolean) => void;
153
- setParams: (patch: ListParamsPatch) => void;
154
- showFilter: (name: string, defaultValue?: unknown) => void;
155
- hideFilter: (name: string) => void;
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
- var DEFAULT_SORT = [{
12
- field: "id",
13
- order: "ASC"
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 { [path]: _flat, ...rest } = filter;
80
+ const result = { ...filter };
81
+ const pruned = result;
82
+ delete pruned[path];
26
83
  const keys = path.split(".");
27
- if (keys.length === 1) return rest;
28
- const prune = (node, depth) => {
29
- const key = keys[depth];
30
- if (key === void 0) return node;
31
- const child = node[key];
32
- if (depth === keys.length - 1) {
33
- const { [key]: _removed, ...others } = node;
34
- return others;
35
- }
36
- if (!isRecord(child)) return node;
37
- const pruned = prune(child, depth + 1);
38
- if (pruned === child) return node;
39
- if (Object.keys(pruned).length === 0) {
40
- const { [key]: _emptied, ...others } = node;
41
- return others;
42
- }
43
- return {
44
- ...node,
45
- [key]: pruned
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, DEFAULT_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: next,
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 { [name]: _shown, ...displayedFilters } = pending?.displayedFilters ?? s.displayedFilters;
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?: Record<string, boolean>;
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: Record<string, boolean>;
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 {};