@urbicon-ui/sveltekit-utils 7.0.0 → 8.0.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/README.md +44 -41
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/table-query.d.ts +16 -62
- package/dist/table-query.js +7 -63
- package/dist/table-view.d.ts +123 -0
- package/dist/table-view.js +241 -0
- package/dist/url.svelte.d.ts +1 -101
- package/dist/url.svelte.js +9 -110
- package/dist/view-binding.svelte.d.ts +73 -0
- package/dist/view-binding.svelte.js +365 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Small, focused SvelteKit helpers that Urbicon apps share. Zero runtime dependenc
|
|
|
5
5
|
Currently shipping:
|
|
6
6
|
|
|
7
7
|
- **URL-state runes** — reactive `useUrlParam` / `useUrlArrayParam` that keep component state in sync with `?query=` parameters
|
|
8
|
-
- **Table
|
|
8
|
+
- **Table view ↔ URL** — `bindViewToUrl`, the URL home for a `@urbicon-ui/table` view object (`?q=…&sort=…&page=…`), plus the pure serializers for the load path
|
|
9
9
|
- **Cron runner** — interval-based background fetcher for scheduled server endpoints
|
|
10
10
|
- **SSE stream reader** — `streamSse`, a spec-correct async-generator client for one-shot POST `text/event-stream` endpoints (LLM relays)
|
|
11
11
|
|
|
@@ -57,60 +57,60 @@ updateUrlSearchParams({ page: '1', tag: ['a', 'b'] }, { replaceState: true });
|
|
|
57
57
|
- URL updates use `goto()` with `replaceState: true`, `noScroll: true`, `keepFocus: true` — suited for filter/pagination UIs, not full page transitions.
|
|
58
58
|
- `useUrlParam` returns getters (not Svelte stores) so consumers can read the value lazily inside `$derived`/`$effect`.
|
|
59
59
|
|
|
60
|
-
## Table
|
|
60
|
+
## Table View ↔ URL (`url.svelte` + `table-query`)
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
`bindViewToUrl` gives the view object of `@urbicon-ui/table` — search, sort, page, page size, filters, grouping — the URL as its home: the axes are mirrored onto query parameters (`?q=…&sort=…&page=…`), so the view survives a reload, can be shared as a link, and — unlike `localStorage` — is visible to the server.
|
|
63
63
|
|
|
64
64
|
```svelte
|
|
65
65
|
<script lang="ts">
|
|
66
|
-
import { Table } from '@urbicon-ui/table';
|
|
67
|
-
import {
|
|
68
|
-
import { createTableQueryUrlSync } from '@urbicon-ui/sveltekit-utils/url.svelte';
|
|
66
|
+
import { Table, createTableView } from '@urbicon-ui/table';
|
|
67
|
+
import { bindViewToUrl } from '@urbicon-ui/sveltekit-utils/url.svelte';
|
|
69
68
|
|
|
70
|
-
const
|
|
69
|
+
const view = createTableView({ defaults: { pageSize: 25 } });
|
|
70
|
+
bindViewToUrl(view);
|
|
71
71
|
</script>
|
|
72
72
|
|
|
73
|
-
<Table
|
|
74
|
-
mode="server"
|
|
75
|
-
{columns}
|
|
76
|
-
itemsPerPage={25}
|
|
77
|
-
query={sync.viewState}
|
|
78
|
-
queryFn={async (query, { signal }) => {
|
|
79
|
-
sync.syncQuery(query); // mirror the query onto the URL (replaceState)
|
|
80
|
-
const res = await fetch(`/api/users?${tableQueryToSearchParams(query)}`, { signal });
|
|
81
|
-
const data = await res.json();
|
|
82
|
-
return { items: data.results, totalItems: data.total };
|
|
83
|
-
}}
|
|
84
|
-
/>
|
|
73
|
+
<Table {items} {columns} {view} />
|
|
85
74
|
```
|
|
86
75
|
|
|
87
|
-
|
|
76
|
+
Both calls belong in the component's initialisation. The init half runs synchronously — a `?sort=…` link renders sorted server HTML — and the runtime halves are effects: URL navigations apply to the view, the reader's changes reach the URL debounced.
|
|
88
77
|
|
|
89
|
-
|
|
78
|
+
The second argument is optional; every option has a default:
|
|
90
79
|
|
|
91
|
-
|
|
80
|
+
| Option | Default | Effect |
|
|
81
|
+
| ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `axes` | all six | Which axes this binding manages. An unbound axis never reaches the URL, whatever the view holds. |
|
|
83
|
+
| `debounceMs` | `300` | Delay before a view change is written to the URL. |
|
|
84
|
+
| `replaceState` | `true` | Replace the current history entry instead of pushing one, so rapid sort/filter/page edits do not flood the back button. |
|
|
85
|
+
| `prefix` | `''` | Key namespace (`prefix: 't_'` → `?t_q=…&t_page=…`) for a second bound table on the same page. |
|
|
86
|
+
| `reflectExternal` | `false` | Mirror an externally applied value (a storage seed) into the URL immediately. Off by default: the address bar does not change without reader interaction, and the seed reaches the URL with the first one anyway. |
|
|
92
87
|
|
|
93
|
-
|
|
88
|
+
Because the binding re-reads the URL rather than capturing it, the browser's back button works: navigating back to a URL that no longer names `?sort` returns the table to its default sort.
|
|
94
89
|
|
|
95
|
-
The pure serializers
|
|
90
|
+
The pure serializers work without SvelteKit — e.g. to parse the incoming query in a server `load` and fetch the first page during SSR. Use `searchParamsToViewQuery` from `./table-view`: it takes the *same* defaults object the component hands `createTableView`, so the server cannot resolve an absent param differently from the client.
|
|
96
91
|
|
|
97
92
|
```typescript
|
|
93
|
+
// view-defaults.ts — imported by both the component and the load function
|
|
94
|
+
export const userView = { pageSize: 25, sort: { column: 'joined', direction: 'desc' } };
|
|
95
|
+
|
|
98
96
|
// +page.server.ts
|
|
99
|
-
import {
|
|
97
|
+
import { searchParamsToViewQuery } from '@urbicon-ui/sveltekit-utils/table-view';
|
|
98
|
+
import { userView } from './view-defaults';
|
|
100
99
|
|
|
101
|
-
export const load = async ({ url }) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
};
|
|
100
|
+
export const load = async ({ url }) => ({
|
|
101
|
+
initialResult: await fetchUsers(searchParamsToViewQuery(url.searchParams, userView))
|
|
102
|
+
});
|
|
105
103
|
```
|
|
106
104
|
|
|
105
|
+
`searchParamsToTableQuery` in `./table-query` does the same job with a baseline in the wire vocabulary (`itemsPerPage`, `sortColumn`/`sortDirection`, `groupByKey`). It has no field for a default filter set, which is why the view-vocabulary function exists.
|
|
106
|
+
|
|
107
107
|
**Design notes**
|
|
108
108
|
|
|
109
|
-
- **Default elision** —
|
|
110
|
-
- **Read tolerant, write strict** — unparsable
|
|
111
|
-
- **Namespacing** — `prefix: 't_'` scopes all keys (`?t_q=…`) for multiple
|
|
112
|
-
- **
|
|
113
|
-
- **
|
|
109
|
+
- **Default elision** — an axis whose value equals its default is not written; a table in its default state leaves the URL clean. The baseline *is* `view.defaults`, read off the object the binding decorates, so there is no second copy of the defaults to keep in step with the table's own.
|
|
110
|
+
- **Read tolerant, write strict** — an unparsable value on a param the URL actually carries falls back to that axis' default, and malformed `filter` entries are skipped individually; the `table-query` serializer throws on a structurally invalid query (non-positive page, unknown operator) instead of writing corrupt state.
|
|
111
|
+
- **Namespacing** — `prefix: 't_'` scopes all keys (`?t_q=…`) for multiple bound tables on one page; unrelated params are always preserved. Two prefixless bindings would manage the same keys, so that throws at registration instead of producing a link that loads the wrong table.
|
|
112
|
+
- **One writer per page** — every binding submits into one coalescing URL writer, so two tables land in a single navigation, each replacing only its own keys. A landing URL the writer itself sent is not applied back onto the view: an edit made while that navigation was in flight survives instead of being overwritten by the URL it raced.
|
|
113
|
+
- **Types** — `TableQueryParams` mirrors the table's `TableQuery`, and `TableViewLike` / `TableViewSnapshot` (from `./table-view`) mirror its view object structurally — so this package carries no dependency on `@urbicon-ui/table`. A parity test in the table package (`tableQuery.parity.test.ts`) pins both mirrors: the snapshot shapes must stay mutually assignable, and the real `TableView` must satisfy `TableViewLike`, which is the entire mechanism by which this binding decorates a view it never imports.
|
|
114
114
|
|
|
115
115
|
## Cron Runner (`cron`)
|
|
116
116
|
|
|
@@ -207,13 +207,16 @@ export const POST = async ({ request }) => {
|
|
|
207
207
|
|
|
208
208
|
## Exports
|
|
209
209
|
|
|
210
|
-
| Subpath | Contents
|
|
211
|
-
| --------------- |
|
|
212
|
-
| `.` | Barrel of all modules
|
|
213
|
-
| `./url.svelte` | `useUrlParam`, `useUrlArrayParam`, `createUrlParam`, `updateUrlSearchParams`, `
|
|
214
|
-
| `./table-
|
|
215
|
-
| `./
|
|
216
|
-
| `./
|
|
210
|
+
| Subpath | Contents |
|
|
211
|
+
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
+
| `.` | Barrel of all modules |
|
|
213
|
+
| `./url.svelte` | `useUrlParam`, `useUrlArrayParam`, `createUrlParam`, `updateUrlSearchParams`, `bindViewToUrl`, types |
|
|
214
|
+
| `./table-view` | `searchParamsToViewQuery`, `searchParamsToViewSnapshot`, `viewSnapshotToTableQuery`, `searchParamsToViewPartial`, `viewSnapshotToSearchParams`, `viewAxesNamedBy`, `viewAxisKeys`, `TABLE_VIEW_AXES`, `TableViewLike`, types |
|
|
215
|
+
| `./table-query` | `tableQueryToSearchParams`, `searchParamsToTableQuery`, `applyTableQueryToSearchParams`, `TABLE_QUERY_FILTER_OPERATORS`, `TableQueryParams`, types |
|
|
216
|
+
| `./cron` | `createCronRunner`, `CronJob`, `CronRunnerConfig`, `CronRunner` |
|
|
217
|
+
| `./sse` | `streamSse`, `SseEvent`, `StreamSseOptions`, `SseRequestError` |
|
|
218
|
+
|
|
219
|
+
`bindViewToUrl` lives in its own module (`view-binding.svelte.ts`) and is re-exported from `./url.svelte`, which is its documented import path — it has no subpath of its own. `./table-view` and `./table-query` are SvelteKit-free (they touch no `$app/*`), which is what lets a `load` function and a plain test use them; `./url.svelte` is the half that needs the router.
|
|
217
220
|
|
|
218
221
|
## Development
|
|
219
222
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/table-query.d.ts
CHANGED
|
@@ -40,8 +40,8 @@ export interface TableQueryFilter {
|
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
42
|
* Query state of a table in server mode. Structural mirror of `TableQuery`
|
|
43
|
-
* from `@urbicon-ui/table` — the object
|
|
44
|
-
* is directly assignable.
|
|
43
|
+
* from `@urbicon-ui/table` — the object a managed `source.query` receives
|
|
44
|
+
* (or `viewToQuery` projects) is directly assignable.
|
|
45
45
|
*/
|
|
46
46
|
export interface TableQueryParams {
|
|
47
47
|
/** Current page (1-based). */
|
|
@@ -59,39 +59,17 @@ export interface TableQueryParams {
|
|
|
59
59
|
/** Column ID for grouping, or null if ungrouped. */
|
|
60
60
|
groupByKey: string | null;
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
63
|
-
* The partial half of {@link TableQueryParams} — structural mirror of
|
|
64
|
-
* `TableViewState` from `@urbicon-ui/table`, the type its `query` prop takes.
|
|
65
|
-
*
|
|
66
|
-
* Every field optional, and that is the whole contract: **presence means
|
|
67
|
-
* controlled**. A consumer may hand over the sort and leave paging to the
|
|
68
|
-
* table, so "absent" has to stay distinguishable from "set to its default
|
|
69
|
-
* value" — which is exactly what {@link TableQueryParams} cannot express.
|
|
70
|
-
*/
|
|
71
|
-
export interface TableQueryViewState {
|
|
72
|
-
/** Current page (1-based). */
|
|
73
|
-
page?: number;
|
|
74
|
-
/** Number of items per page. */
|
|
75
|
-
itemsPerPage?: number;
|
|
76
|
-
/** Column ID to sort by, or empty string for no sort. */
|
|
77
|
-
sortColumn?: string;
|
|
78
|
-
/** Sort direction. */
|
|
79
|
-
sortDirection?: TableQuerySortDirection;
|
|
80
|
-
/** Full-text search term. */
|
|
81
|
-
searchTerm?: string;
|
|
82
|
-
/** Active column filters. */
|
|
83
|
-
activeFilters?: TableQueryFilter[];
|
|
84
|
-
/** Column ID for grouping, or null for ungrouped. */
|
|
85
|
-
groupByKey?: string | null;
|
|
86
|
-
}
|
|
87
62
|
/**
|
|
88
63
|
* Baseline used for default elision: query values equal to these defaults are
|
|
89
64
|
* omitted from the URL, and missing params parse back to them.
|
|
90
65
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
66
|
+
* A **wire-codec** baseline in the query shape — when your defaults live on a
|
|
67
|
+
* view object (`createTableView({ defaults })`), skip the manual projection
|
|
68
|
+
* and use `searchParamsToViewQuery` from `./table-view`, which takes the view
|
|
69
|
+
* itself. Unset fields fall back to the table's own defaults (page 1, 10
|
|
70
|
+
* items per page, no sort, empty search, ungrouped). Filters have no default
|
|
71
|
+
* field: active filters are always written, an absent `filter` param parses
|
|
72
|
+
* to none.
|
|
95
73
|
*/
|
|
96
74
|
export interface TableQueryDefaults {
|
|
97
75
|
/** Default page. @default 1 */
|
|
@@ -136,8 +114,8 @@ export interface TableQueryUrlOptions {
|
|
|
136
114
|
* When `sortColumn` is empty the sort direction is meaningless and is
|
|
137
115
|
* normalized away (it parses back as `'asc'`).
|
|
138
116
|
*
|
|
139
|
-
* Also handy for building the backend request inside
|
|
140
|
-
* scheme works as an API query string.
|
|
117
|
+
* Also handy for building the backend request inside a managed
|
|
118
|
+
* `source.query` — the same scheme works as an API query string.
|
|
141
119
|
*
|
|
142
120
|
* @param query - Query emitted by the table (`TableQuery` is assignable).
|
|
143
121
|
* @param options - Elision defaults + key prefix.
|
|
@@ -153,41 +131,17 @@ export declare function tableQueryToSearchParams(query: TableQueryParams, option
|
|
|
153
131
|
* unknown `dir` becomes `'asc'`, and malformed `filter` entries (wrong shape,
|
|
154
132
|
* unknown operator, broken percent-encoding) are skipped individually.
|
|
155
133
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
134
|
+
* The wire-level codec, for consumers who hold their defaults in the query
|
|
135
|
+
* shape. For the SSR load path of a v8 view — running the initial fetch in a
|
|
136
|
+
* server `load` from `url.searchParams` — prefer `searchParamsToViewQuery`
|
|
137
|
+
* from `./table-view`: same keys, but it reads the defaults off the view
|
|
138
|
+
* object instead of a hand-projected baseline.
|
|
158
139
|
*
|
|
159
140
|
* @param params - Search params to read (not mutated).
|
|
160
141
|
* @param options - Fallback defaults + key prefix.
|
|
161
142
|
* @returns Complete query object (assignable to `TableQuery`).
|
|
162
143
|
*/
|
|
163
144
|
export declare function searchParamsToTableQuery(params: URLSearchParams, options?: TableQueryUrlOptions): TableQueryParams;
|
|
164
|
-
/**
|
|
165
|
-
* Parse `URLSearchParams` into the **partial** view state a table's `query`
|
|
166
|
-
* prop takes — a key per axis the URL actually carries, and nothing else.
|
|
167
|
-
*
|
|
168
|
-
* This is the deliberate opposite of {@link searchParamsToTableQuery}, and the
|
|
169
|
-
* distinction is load-bearing rather than cosmetic. The table reads `query` by
|
|
170
|
-
* **field presence**: a present field means "this axis is controlled — outrank
|
|
171
|
-
* persistence and the `initial*` seed"; an absent one means "the URL has no
|
|
172
|
-
* opinion, carry on". A complete object therefore claims every axis, so
|
|
173
|
-
* handing `searchParamsToTableQuery`'s output to `query` silently switches off
|
|
174
|
-
* `persistenceConfig`, `initialSort`, `initialFilters` and `initialGroupBy` —
|
|
175
|
-
* even on a URL with no params at all, where its every field is a default it
|
|
176
|
-
* invented. That is not a hypothetical: it was the shipped wiring, and this
|
|
177
|
-
* function exists because of it.
|
|
178
|
-
*
|
|
179
|
-
* `sortColumn` and `sortDirection` are emitted as a pair or not at all, so the
|
|
180
|
-
* half-controlled sort (a direction with no column) cannot be produced here.
|
|
181
|
-
*
|
|
182
|
-
* Same tolerance as the full parser: an unparsable `page`/`size` falls back to
|
|
183
|
-
* the resolved default *for that key* — the key was present, so the axis stays
|
|
184
|
-
* controlled — and malformed filters are skipped individually.
|
|
185
|
-
*
|
|
186
|
-
* @param params - Search params to read (not mutated).
|
|
187
|
-
* @param options - Fallback defaults + key prefix.
|
|
188
|
-
* @returns Only the axes present in `params`.
|
|
189
|
-
*/
|
|
190
|
-
export declare function searchParamsToTableViewState(params: URLSearchParams, options?: TableQueryUrlOptions): TableQueryViewState;
|
|
191
145
|
/**
|
|
192
146
|
* Merge a table query into existing search params: all managed keys (`q`,
|
|
193
147
|
* `page`, `size`, `sort`, `dir`, `group`, `filter` — with the configured
|
package/dist/table-query.js
CHANGED
|
@@ -128,8 +128,8 @@ function parseFilterParam(raw) {
|
|
|
128
128
|
* When `sortColumn` is empty the sort direction is meaningless and is
|
|
129
129
|
* normalized away (it parses back as `'asc'`).
|
|
130
130
|
*
|
|
131
|
-
* Also handy for building the backend request inside
|
|
132
|
-
* scheme works as an API query string.
|
|
131
|
+
* Also handy for building the backend request inside a managed
|
|
132
|
+
* `source.query` — the same scheme works as an API query string.
|
|
133
133
|
*
|
|
134
134
|
* @param query - Query emitted by the table (`TableQuery` is assignable).
|
|
135
135
|
* @param options - Elision defaults + key prefix.
|
|
@@ -172,8 +172,11 @@ export function tableQueryToSearchParams(query, options = {}) {
|
|
|
172
172
|
* unknown `dir` becomes `'asc'`, and malformed `filter` entries (wrong shape,
|
|
173
173
|
* unknown operator, broken percent-encoding) are skipped individually.
|
|
174
174
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
175
|
+
* The wire-level codec, for consumers who hold their defaults in the query
|
|
176
|
+
* shape. For the SSR load path of a v8 view — running the initial fetch in a
|
|
177
|
+
* server `load` from `url.searchParams` — prefer `searchParamsToViewQuery`
|
|
178
|
+
* from `./table-view`: same keys, but it reads the defaults off the view
|
|
179
|
+
* object instead of a hand-projected baseline.
|
|
177
180
|
*
|
|
178
181
|
* @param params - Search params to read (not mutated).
|
|
179
182
|
* @param options - Fallback defaults + key prefix.
|
|
@@ -206,65 +209,6 @@ export function searchParamsToTableQuery(params, options = {}) {
|
|
|
206
209
|
groupByKey: rawGroup !== null ? (rawGroup === '' ? null : rawGroup) : d.groupByKey
|
|
207
210
|
};
|
|
208
211
|
}
|
|
209
|
-
/**
|
|
210
|
-
* Parse `URLSearchParams` into the **partial** view state a table's `query`
|
|
211
|
-
* prop takes — a key per axis the URL actually carries, and nothing else.
|
|
212
|
-
*
|
|
213
|
-
* This is the deliberate opposite of {@link searchParamsToTableQuery}, and the
|
|
214
|
-
* distinction is load-bearing rather than cosmetic. The table reads `query` by
|
|
215
|
-
* **field presence**: a present field means "this axis is controlled — outrank
|
|
216
|
-
* persistence and the `initial*` seed"; an absent one means "the URL has no
|
|
217
|
-
* opinion, carry on". A complete object therefore claims every axis, so
|
|
218
|
-
* handing `searchParamsToTableQuery`'s output to `query` silently switches off
|
|
219
|
-
* `persistenceConfig`, `initialSort`, `initialFilters` and `initialGroupBy` —
|
|
220
|
-
* even on a URL with no params at all, where its every field is a default it
|
|
221
|
-
* invented. That is not a hypothetical: it was the shipped wiring, and this
|
|
222
|
-
* function exists because of it.
|
|
223
|
-
*
|
|
224
|
-
* `sortColumn` and `sortDirection` are emitted as a pair or not at all, so the
|
|
225
|
-
* half-controlled sort (a direction with no column) cannot be produced here.
|
|
226
|
-
*
|
|
227
|
-
* Same tolerance as the full parser: an unparsable `page`/`size` falls back to
|
|
228
|
-
* the resolved default *for that key* — the key was present, so the axis stays
|
|
229
|
-
* controlled — and malformed filters are skipped individually.
|
|
230
|
-
*
|
|
231
|
-
* @param params - Search params to read (not mutated).
|
|
232
|
-
* @param options - Fallback defaults + key prefix.
|
|
233
|
-
* @returns Only the axes present in `params`.
|
|
234
|
-
*/
|
|
235
|
-
export function searchParamsToTableViewState(params, options = {}) {
|
|
236
|
-
const d = resolveDefaults(options.defaults);
|
|
237
|
-
const k = paramKeys(options.prefix ?? '');
|
|
238
|
-
const view = {};
|
|
239
|
-
const rawPage = params.get(k.page);
|
|
240
|
-
if (rawPage !== null)
|
|
241
|
-
view.page = parsePositiveInt(rawPage) ?? d.page;
|
|
242
|
-
const rawSize = params.get(k.size);
|
|
243
|
-
if (rawSize !== null)
|
|
244
|
-
view.itemsPerPage = parsePositiveInt(rawSize) ?? d.itemsPerPage;
|
|
245
|
-
const rawSort = params.get(k.sort);
|
|
246
|
-
if (rawSort !== null) {
|
|
247
|
-
view.sortColumn = rawSort;
|
|
248
|
-
view.sortDirection = rawSort !== '' && params.get(k.dir) === 'desc' ? 'desc' : 'asc';
|
|
249
|
-
}
|
|
250
|
-
const rawSearch = params.get(k.q);
|
|
251
|
-
if (rawSearch !== null)
|
|
252
|
-
view.searchTerm = rawSearch;
|
|
253
|
-
const rawFilters = params.getAll(k.filter);
|
|
254
|
-
if (rawFilters.length > 0) {
|
|
255
|
-
const activeFilters = [];
|
|
256
|
-
for (const raw of rawFilters) {
|
|
257
|
-
const filter = parseFilterParam(raw);
|
|
258
|
-
if (filter)
|
|
259
|
-
activeFilters.push(filter);
|
|
260
|
-
}
|
|
261
|
-
view.activeFilters = activeFilters;
|
|
262
|
-
}
|
|
263
|
-
const rawGroup = params.get(k.group);
|
|
264
|
-
if (rawGroup !== null)
|
|
265
|
-
view.groupByKey = rawGroup === '' ? null : rawGroup;
|
|
266
|
-
return view;
|
|
267
|
-
}
|
|
268
212
|
/**
|
|
269
213
|
* Merge a table query into existing search params: all managed keys (`q`,
|
|
270
214
|
* `page`, `size`, `sort`, `dir`, `group`, `filter` — with the configured
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL (de)serialization for the v8 table view vocabulary (`TableView` from
|
|
3
|
+
* `@urbicon-ui/table`: search, sort, page, pageSize, filters, groupBy).
|
|
4
|
+
*
|
|
5
|
+
* The types in this module are a **structural mirror** of the table package's
|
|
6
|
+
* view types — deliberately not imported, so this package carries no
|
|
7
|
+
* dependency on `@urbicon-ui/table`. A type-parity test over there guards the
|
|
8
|
+
* shapes against drift; `bindViewToUrl` accepts any object shaped like
|
|
9
|
+
* {@link TableViewLike}, which the real `TableView` is.
|
|
10
|
+
*
|
|
11
|
+
* The key scheme is the shipped one (`q`, `page`, `size`, `sort`, `dir`,
|
|
12
|
+
* `group`, `filter`) — deep links written for v7 keep parsing. The write
|
|
13
|
+
* side adds one compatible extension: an empty `filter=` marker, analogous
|
|
14
|
+
* to `sort=`, so a cleared filter set elides like every other axis (the
|
|
15
|
+
* shipped read side already tolerated it).
|
|
16
|
+
*/
|
|
17
|
+
import { type TableQueryFilter, type TableQueryParams } from './table-query.js';
|
|
18
|
+
/** One of the six view axes. Mirrors `ViewAxis` from `@urbicon-ui/table`. */
|
|
19
|
+
export type TableViewAxis = 'search' | 'sort' | 'page' | 'pageSize' | 'filters' | 'groupBy';
|
|
20
|
+
/** All six view axes, in vocabulary order. */
|
|
21
|
+
export declare const TABLE_VIEW_AXES: readonly TableViewAxis[];
|
|
22
|
+
/** Sort state of a view. Mirrors `ViewSort` from `@urbicon-ui/table`. */
|
|
23
|
+
export interface TableViewSort {
|
|
24
|
+
/** Column ID to sort by. */
|
|
25
|
+
column: string;
|
|
26
|
+
/** Sort direction. */
|
|
27
|
+
direction: 'asc' | 'desc';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A fully resolved view state — never `undefined` anywhere. Mirrors
|
|
31
|
+
* `TableViewSnapshot` from `@urbicon-ui/table`.
|
|
32
|
+
*/
|
|
33
|
+
export interface TableViewSnapshot {
|
|
34
|
+
search: string;
|
|
35
|
+
sort: TableViewSort | null;
|
|
36
|
+
page: number;
|
|
37
|
+
pageSize: number;
|
|
38
|
+
filters: TableQueryFilter[];
|
|
39
|
+
groupBy: string | null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The surface {@link bindViewToUrl} needs from a view object — a structural
|
|
43
|
+
* mirror of the table package's `TableView` class. Field reads are reactive,
|
|
44
|
+
* field writes count as the reader's own change; `applyExternal` is the
|
|
45
|
+
* binding write surface, `claimAxes`/`releaseAxes` the fail-loud composition
|
|
46
|
+
* registry, and `originOf` the per-axis (revision, origin) bookkeeping the
|
|
47
|
+
* bindings decide by.
|
|
48
|
+
*/
|
|
49
|
+
export interface TableViewLike {
|
|
50
|
+
readonly defaults: TableViewSnapshot;
|
|
51
|
+
search: string;
|
|
52
|
+
sort: TableViewSort | null;
|
|
53
|
+
page: number;
|
|
54
|
+
pageSize: number;
|
|
55
|
+
filters: TableQueryFilter[];
|
|
56
|
+
groupBy: string | null;
|
|
57
|
+
applyExternal(partial: Partial<TableViewSnapshot>, origin: 'external' | 'system'): void;
|
|
58
|
+
claimAxes(kind: 'url' | 'storage', axes: readonly TableViewAxis[]): void;
|
|
59
|
+
releaseAxes(kind: 'url' | 'storage', axes: readonly TableViewAxis[]): void;
|
|
60
|
+
markInitApplied(axes: readonly TableViewAxis[]): void;
|
|
61
|
+
wasInitApplied(axis: TableViewAxis): boolean;
|
|
62
|
+
originOf(axis: TableViewAxis): {
|
|
63
|
+
revision: number;
|
|
64
|
+
origin: 'user' | 'external' | 'system' | 'init';
|
|
65
|
+
};
|
|
66
|
+
snapshot(): TableViewSnapshot;
|
|
67
|
+
}
|
|
68
|
+
/** The URL keys a set of axes owns, with the configured prefix applied. */
|
|
69
|
+
export declare function viewAxisKeys(axes: readonly TableViewAxis[], prefix?: string): string[];
|
|
70
|
+
/** The axes a URL names — presence only for params it actually carries. */
|
|
71
|
+
export declare function viewAxesNamedBy(sp: URLSearchParams, prefix?: string): TableViewAxis[];
|
|
72
|
+
/**
|
|
73
|
+
* Parse search params into a **partial** view snapshot — a key per axis the
|
|
74
|
+
* URL actually carries, and nothing else. Read tolerant per key: an
|
|
75
|
+
* unparsable value on a *present* key falls back to the configured default
|
|
76
|
+
* for that axis (the key was present, so the axis stays claimed), and
|
|
77
|
+
* malformed filter entries are skipped individually.
|
|
78
|
+
*/
|
|
79
|
+
export declare function searchParamsToViewPartial(sp: URLSearchParams, defaults: Pick<TableViewSnapshot, 'page' | 'pageSize'>, prefix?: string): Partial<TableViewSnapshot>;
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a full view snapshot from search params: every axis the URL names
|
|
82
|
+
* comes from the URL, every other one from `defaults` — the same resolution
|
|
83
|
+
* the URL binding performs at init, for code that has no view (a server
|
|
84
|
+
* `load`).
|
|
85
|
+
*/
|
|
86
|
+
export declare function searchParamsToViewSnapshot(sp: URLSearchParams, defaults?: Partial<TableViewSnapshot>, prefix?: string): TableViewSnapshot;
|
|
87
|
+
/**
|
|
88
|
+
* Project a view snapshot into the wire shape a backend speaks — the same
|
|
89
|
+
* mapping the table applies before calling a managed `source.query`, so a
|
|
90
|
+
* server `load` and the table's own fetches send identical field names.
|
|
91
|
+
*/
|
|
92
|
+
export declare function viewSnapshotToTableQuery(snapshot: TableViewSnapshot): TableQueryParams;
|
|
93
|
+
/**
|
|
94
|
+
* The load-path counterpart of the URL binding: parse search params against
|
|
95
|
+
* the **view's own defaults** and hand back the query a fetch needs.
|
|
96
|
+
*
|
|
97
|
+
* The point is the defaults argument. `searchParamsToTableQuery` takes its
|
|
98
|
+
* baseline in the wire vocabulary (`itemsPerPage`, `sortColumn`/`sortDirection`,
|
|
99
|
+
* `groupByKey`) and has no field for filters at all, so a `load` had to keep a
|
|
100
|
+
* second, differently-spelled copy of what `createTableView({ defaults })`
|
|
101
|
+
* already says — and could not express a default filter set no matter how it
|
|
102
|
+
* was written (#157 finding 2). This one takes the very object the view takes:
|
|
103
|
+
* one spelling, all six axes, so the server cannot resolve an absent param
|
|
104
|
+
* differently from the client.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* // shared with the component that calls createTableView({ defaults })
|
|
109
|
+
* export const invoiceView = { pageSize: 25, sort: { column: 'date', direction: 'desc' } };
|
|
110
|
+
*
|
|
111
|
+
* export const load = async ({ url }) => ({
|
|
112
|
+
* initialResult: await fetchInvoices(searchParamsToViewQuery(url.searchParams, invoiceView))
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare function searchParamsToViewQuery(sp: URLSearchParams, defaults?: Partial<TableViewSnapshot>, prefix?: string): TableQueryParams;
|
|
117
|
+
/**
|
|
118
|
+
* Serialize a snapshot, eliding every axis that equals the defaults — the
|
|
119
|
+
* elision baseline *is* the view's defaults, structurally. `axes` restricts
|
|
120
|
+
* the output to a binding's own axes: an unbound axis never reaches the URL,
|
|
121
|
+
* no matter what the view holds.
|
|
122
|
+
*/
|
|
123
|
+
export declare function viewSnapshotToSearchParams(snapshot: TableViewSnapshot, defaults: TableViewSnapshot, axes?: readonly TableViewAxis[], prefix?: string): URLSearchParams;
|