@atscript/ui-table 0.1.58
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/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/index.cjs +1689 -0
- package/dist/index.d.cts +1034 -0
- package/dist/index.d.mts +1034 -0
- package/dist/index.mjs +1622 -0
- package/package.json +63 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
import { FilterExpr, Uniquery } from "@uniqu/core";
|
|
2
|
+
import { ColumnDef, PaginationControl, SortControl, TableDef } from "@atscript/ui";
|
|
3
|
+
import { Client } from "@atscript/db-client";
|
|
4
|
+
|
|
5
|
+
//#region src/filters/filter-types.d.ts
|
|
6
|
+
/** Filter condition type identifiers. */
|
|
7
|
+
type FilterConditionType = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | "starts" | "ends" | "bw" | "null" | "notNull" | "regex";
|
|
8
|
+
/**
|
|
9
|
+
* A single filter condition applied to a field.
|
|
10
|
+
*
|
|
11
|
+
* - Most conditions use `value[0]`.
|
|
12
|
+
* - `bw` (between) uses `value[0]` (low) and `value[1]` (high).
|
|
13
|
+
* - `null`/`notNull` ignore value.
|
|
14
|
+
*/
|
|
15
|
+
interface FilterCondition {
|
|
16
|
+
type: FilterConditionType;
|
|
17
|
+
value: (string | number | boolean)[];
|
|
18
|
+
}
|
|
19
|
+
/** Filter conditions grouped by field path. */
|
|
20
|
+
type FieldFilters = Record<string, FilterCondition[]>;
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/filters/filter-conditions.d.ts
|
|
23
|
+
/** Conditions that operate purely on nullability — value is ignored. */
|
|
24
|
+
declare const NULL_OPS: ReadonlySet<FilterConditionType>;
|
|
25
|
+
/** Check if a condition has a filled/meaningful value. */
|
|
26
|
+
declare function isFilled(condition: FilterCondition): boolean;
|
|
27
|
+
/** Check if a condition type requires a second value (between). */
|
|
28
|
+
declare function hasSecondValue(type: FilterConditionType): boolean;
|
|
29
|
+
declare function isSimpleEq(condition: FilterCondition): boolean;
|
|
30
|
+
/** Human-readable label for a condition type. */
|
|
31
|
+
declare function conditionLabel(type: FilterConditionType): string;
|
|
32
|
+
/** Count of fields that have at least one filled condition. */
|
|
33
|
+
declare function filledFilterCount(filters: FieldFilters): number;
|
|
34
|
+
/** Summarize a field's conditions into a human-readable token label. */
|
|
35
|
+
declare function filterTokenLabel(path: string, conditions: FilterCondition[], columnLabel?: string): string;
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/filters/filter-conditions-map.d.ts
|
|
38
|
+
/** Column type categories for condition availability. */
|
|
39
|
+
type ColumnFilterType = "text" | "number" | "date" | "boolean" | "enum" | "ref";
|
|
40
|
+
/**
|
|
41
|
+
* Available filter conditions for a given column filter type.
|
|
42
|
+
* Non-nullable columns drop `null` / `notNull` since they can never match.
|
|
43
|
+
*/
|
|
44
|
+
declare function conditionsForType(type: ColumnFilterType, nullable?: boolean): readonly FilterConditionType[];
|
|
45
|
+
/** Map a ColumnDef display type string to a ColumnFilterType. */
|
|
46
|
+
declare function columnFilterType(columnType: string): ColumnFilterType;
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/filters/escape-regex.d.ts
|
|
49
|
+
/** Escape special regex characters in user input for safe embedding in $regex. */
|
|
50
|
+
declare function escapeRegex(input: string): string;
|
|
51
|
+
/** Reverse of {@link escapeRegex}. */
|
|
52
|
+
declare function unescapeRegex(input: string): string;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/filters/filter-input-format.d.ts
|
|
55
|
+
/** Default condition type when no symbol matches the input. */
|
|
56
|
+
declare function defaultCondition(columnType: ColumnFilterType): FilterConditionType;
|
|
57
|
+
/**
|
|
58
|
+
* Parse a user-typed filter input string into a FilterCondition.
|
|
59
|
+
*
|
|
60
|
+
* Supports operator symbols:
|
|
61
|
+
* *text* → contains
|
|
62
|
+
* text* → starts with
|
|
63
|
+
* *text → ends with
|
|
64
|
+
* =value → eq (explicit)
|
|
65
|
+
* !=value → ne
|
|
66
|
+
* >=value → gte
|
|
67
|
+
* >value → gt
|
|
68
|
+
* <=value → lte
|
|
69
|
+
* <value → lt
|
|
70
|
+
* lo...hi → bw (between)
|
|
71
|
+
* <empty> → null
|
|
72
|
+
* !<empty> → notNull
|
|
73
|
+
* /pattern/ → regex
|
|
74
|
+
*
|
|
75
|
+
* When no symbol matches, the default depends on columnType:
|
|
76
|
+
* text/enum/ref → contains
|
|
77
|
+
* number/date/boolean → eq
|
|
78
|
+
*
|
|
79
|
+
* Returns undefined for empty/invalid input or if the parsed operator
|
|
80
|
+
* is not available for the column type.
|
|
81
|
+
*/
|
|
82
|
+
declare function parseFilterInput(text: string, columnType: ColumnFilterType, nullable?: boolean): FilterCondition | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Format a FilterCondition for chip/token display using operator symbols.
|
|
85
|
+
*
|
|
86
|
+
* Round-trips with parseFilterInput:
|
|
87
|
+
* formatFilterCondition(parseFilterInput(text, type)) ≈ text
|
|
88
|
+
*/
|
|
89
|
+
declare function formatFilterCondition(condition: FilterCondition): string;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/filters/filters-to-uniquery.d.ts
|
|
92
|
+
/**
|
|
93
|
+
* Convert UI filter model to a Uniquery FilterExpr.
|
|
94
|
+
*
|
|
95
|
+
* Combination logic:
|
|
96
|
+
* - Per field: inclusion (positive) conditions are OR'd, exclusion (negative) conditions are AND'd.
|
|
97
|
+
* - Across fields: all groups are AND'd at the top level.
|
|
98
|
+
*
|
|
99
|
+
* Returns `undefined` when no filled conditions exist.
|
|
100
|
+
*/
|
|
101
|
+
declare function filtersToUniqueryFilter(fieldFilters: FieldFilters): FilterExpr | undefined;
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/filters/uniquery-to-filters.d.ts
|
|
104
|
+
/**
|
|
105
|
+
* Convert a Uniquery `FilterExpr` back into the UI's `FieldFilters` shape.
|
|
106
|
+
*
|
|
107
|
+
* Inverse of `filtersToUniqueryFilter`. Drops:
|
|
108
|
+
* - conditions on fields not in `knownFields` (when provided)
|
|
109
|
+
* - operators not in the supported `FilterConditionType` union
|
|
110
|
+
* - `$not` branches (no native UI representation; lossy on hand-crafted URLs)
|
|
111
|
+
*
|
|
112
|
+
* Returns `{}` for an empty/missing expression. Never throws.
|
|
113
|
+
*/
|
|
114
|
+
declare function uniqueryFilterToFieldFilters(expr: FilterExpr | undefined, knownFields?: Iterable<string> | Set<string>): FieldFilters;
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/filters/date-shortcuts.d.ts
|
|
117
|
+
/** A date shortcut produces a label and a [start, end] ISO date range. */
|
|
118
|
+
interface DateShortcut {
|
|
119
|
+
label: string;
|
|
120
|
+
dates: [start: string, end: string];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Generate date filter shortcuts relative to the given date (defaults to now).
|
|
124
|
+
* Each shortcut produces a `bw` (between) condition range using ISO date strings (YYYY-MM-DD).
|
|
125
|
+
*/
|
|
126
|
+
declare function dateShortcuts(now?: Date): DateShortcut[];
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/presets/preset-types.d.ts
|
|
129
|
+
/**
|
|
130
|
+
* In-memory snapshot of table state for preset persistence. Dict-shaped
|
|
131
|
+
* aspects (`columns.columnWidths`, `filterOps`) match the runtime state
|
|
132
|
+
* shape. The wire form (entries-arrays for atscript validation) is
|
|
133
|
+
* `PresetSnapshotWire` in `./preset-wire-types`.
|
|
134
|
+
*
|
|
135
|
+
* Per-aspect opt-in: a key's presence claims that aspect; absent keys are
|
|
136
|
+
* left untouched on apply.
|
|
137
|
+
*/
|
|
138
|
+
interface PresetSnapshot {
|
|
139
|
+
columns?: {
|
|
140
|
+
columnNames: string[]; /** Override-only diff against column defaults. Never serialise the default. */
|
|
141
|
+
columnWidths?: Record<string, string>;
|
|
142
|
+
};
|
|
143
|
+
/** Displayed filter field paths (the visible-input list). */
|
|
144
|
+
filters?: string[];
|
|
145
|
+
/** Applied filter conditions, dict keyed by field path. */
|
|
146
|
+
filterOps?: FieldFilters;
|
|
147
|
+
sorters?: SortControl[];
|
|
148
|
+
itemsPerPage?: number;
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/presets/preset-wire-types.d.ts
|
|
152
|
+
interface PresetColumnWidthEntry {
|
|
153
|
+
field: string;
|
|
154
|
+
width: string;
|
|
155
|
+
}
|
|
156
|
+
interface PresetFilterOpEntry {
|
|
157
|
+
field: string;
|
|
158
|
+
conditions: FilterCondition[];
|
|
159
|
+
}
|
|
160
|
+
interface PresetSorterEntry {
|
|
161
|
+
field: string;
|
|
162
|
+
direction: "asc" | "desc";
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Wire-format snapshot stored in `AsPresetEntry.data.content`. Dict-shaped
|
|
166
|
+
* in-memory aspects (`columnWidths`, `filterOps`) become entries-arrays at
|
|
167
|
+
* the serializer boundary so atscript can validate them strictly — atscript
|
|
168
|
+
* has no `Record<string, T>` / `additionalProperties` construct.
|
|
169
|
+
*
|
|
170
|
+
* The client-side dict form is `PresetSnapshot` in `./preset-types`. Use
|
|
171
|
+
* `toWireSnapshot` / `fromWireSnapshot` (in `./preset-wire`) to bridge them.
|
|
172
|
+
*/
|
|
173
|
+
interface PresetSnapshotWire {
|
|
174
|
+
columns?: {
|
|
175
|
+
columnNames: string[];
|
|
176
|
+
columnWidths?: PresetColumnWidthEntry[];
|
|
177
|
+
};
|
|
178
|
+
filters?: string[];
|
|
179
|
+
filterOps?: PresetFilterOpEntry[];
|
|
180
|
+
sorters?: PresetSorterEntry[];
|
|
181
|
+
itemsPerPage?: number;
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/presets/preset-aspects.d.ts
|
|
185
|
+
declare const PRESET_ASPECTS: readonly ["columns", "filters", "filterOps", "sorters", "itemsPerPage"];
|
|
186
|
+
type PresetAspect = (typeof PRESET_ASPECTS)[number];
|
|
187
|
+
/**
|
|
188
|
+
* Per-aspect opt-in/out for `captureSnapshot(mask)`. A `true` flag includes
|
|
189
|
+
* the aspect; `false` / absent excludes. The capture filter intersects the
|
|
190
|
+
* mask with `availablePresetAspects` so unavailable aspects never leak in.
|
|
191
|
+
*/
|
|
192
|
+
type AspectMask = Partial<Record<PresetAspect, boolean>>;
|
|
193
|
+
declare function derivePresetAspects(content: unknown): PresetAspect[];
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/presets/preset-wire.d.ts
|
|
196
|
+
/**
|
|
197
|
+
* Convert in-memory dict-form snapshot to the wire form persisted on the
|
|
198
|
+
* server. Entries-arrays are sorted by `field` so consumers (server-side
|
|
199
|
+
* aspect derivation, dirty detection, equality checks) see a stable order.
|
|
200
|
+
*/
|
|
201
|
+
declare function toWireSnapshot(snapshot: PresetSnapshot): PresetSnapshotWire;
|
|
202
|
+
declare function fromWireSnapshot(wire: PresetSnapshotWire): PresetSnapshot;
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/presets/preset-data-types.d.ts
|
|
205
|
+
/**
|
|
206
|
+
* Wire-shape `data` payload for `type='preset'` rows on `AsPresetEntry`. The
|
|
207
|
+
* server validates `content` against the wire shape; the client converts to
|
|
208
|
+
* dict-form `PresetSnapshot` via `fromWireSnapshot` for in-memory use.
|
|
209
|
+
*/
|
|
210
|
+
interface PresetData {
|
|
211
|
+
/** User-visible label. Public-name uniqueness within `(app, tableKey)`. */
|
|
212
|
+
label: string;
|
|
213
|
+
/**
|
|
214
|
+
* Wire-form snapshot. The set of present top-level keys drives the
|
|
215
|
+
* top-level `aspects` column on the row — derived automatically on every
|
|
216
|
+
* preset write so picker queries can project `aspects` without loading
|
|
217
|
+
* `data`.
|
|
218
|
+
*/
|
|
219
|
+
content?: PresetSnapshotWire;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Per-user, per-table configuration row (`type='userConf'`, deterministic
|
|
223
|
+
* id `uc:${user}:${app}:${tableKey}`). Carries preferences that are
|
|
224
|
+
* inherently per-table (preset pins / favorites); app-wide prefs live on
|
|
225
|
+
* `AppConfData`.
|
|
226
|
+
*/
|
|
227
|
+
interface UserConfData {
|
|
228
|
+
/**
|
|
229
|
+
* Stable preset id (or `'sys:<id>'` for system presets) pinned as the
|
|
230
|
+
* default for this table. Resolution is tolerant — a stale id is left
|
|
231
|
+
* intact and may reactivate if the referenced preset returns.
|
|
232
|
+
*/
|
|
233
|
+
defaultPresetId?: string;
|
|
234
|
+
favPresetIds?: string[];
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Per-user, app-wide configuration row (`type='appConf'`, deterministic id
|
|
238
|
+
* `ac:${user}:${app}`). Holds preferences that are inherently app-scoped —
|
|
239
|
+
* duplicating per-table would let them drift.
|
|
240
|
+
*/
|
|
241
|
+
interface AppConfData {
|
|
242
|
+
appearance?: "system" | "light" | "dark";
|
|
243
|
+
/** BCP-47 language tag (`'en'`, `'en-US'`). Max 5 chars. */
|
|
244
|
+
language?: string;
|
|
245
|
+
/** IANA timezone name (`'America/New_York'`). Max 64 chars. */
|
|
246
|
+
timezone?: string;
|
|
247
|
+
density?: "compact" | "cozy" | "comfortable";
|
|
248
|
+
dateFormat?: "iso" | "us" | "eu";
|
|
249
|
+
/** 0 = Sunday, 1 = Monday, 6 = Saturday. */
|
|
250
|
+
firstDayOfWeek?: 0 | 1 | 6;
|
|
251
|
+
/** Escape hatch for app-specific user prefs. Capped at 1024 chars. */
|
|
252
|
+
customJson?: string;
|
|
253
|
+
}
|
|
254
|
+
type AsPresetEntryData = PresetData | UserConfData | AppConfData;
|
|
255
|
+
type AsPresetsErrorCode = "preset_limit_reached" | "reserved_id" | "public_name_conflict" | "missing_scope" | "missing_id" | "invalid_type" | "type_immutable" | "identity_immutable" | "preset_not_found" | "publish_forbidden" | "action_unsupported";
|
|
256
|
+
/** 409 body returned when a user's per-`(app, tableKey)` preset cap would be exceeded. */
|
|
257
|
+
interface PresetLimitReachedBody {
|
|
258
|
+
code: "preset_limit_reached";
|
|
259
|
+
limit: number;
|
|
260
|
+
count: number;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Browser-side row shape for `AsPresetEntry`. The server-authoritative
|
|
264
|
+
* shape lives in `@atscript/moost-ui-presets`'s generated `.as.d.ts`; this
|
|
265
|
+
* type duplicates the runtime view so client code (composables, picker,
|
|
266
|
+
* dialog) never imports the server-only package.
|
|
267
|
+
*/
|
|
268
|
+
interface AsPresetEntryRow {
|
|
269
|
+
id: string;
|
|
270
|
+
type: "preset" | "userConf" | "appConf";
|
|
271
|
+
app: string;
|
|
272
|
+
tableKey?: string;
|
|
273
|
+
user: string;
|
|
274
|
+
/**
|
|
275
|
+
* Display label for `user` (e.g. their username) — server-stamped on every
|
|
276
|
+
* write via `AsPresetsController.getUserLabel`. Optional: when the
|
|
277
|
+
* controller doesn't override the hook, this is `undefined` and surfaces
|
|
278
|
+
* fall back to rendering `user`.
|
|
279
|
+
*/
|
|
280
|
+
userLabel?: string;
|
|
281
|
+
/** Preset-only top-level mirror; controller stamps on every preset write. */
|
|
282
|
+
public?: boolean;
|
|
283
|
+
/** Preset-only top-level mirror of `data.label`; stamped by controller. */
|
|
284
|
+
label?: string;
|
|
285
|
+
/** Equals `label` when `public=true`, else absent. Composite-unique on `(app,tableKey,publicLabel)`. */
|
|
286
|
+
publicLabel?: string;
|
|
287
|
+
/** Derived by the controller from `data.content` keys. */
|
|
288
|
+
aspects?: PresetAspect[];
|
|
289
|
+
data: AsPresetEntryData;
|
|
290
|
+
createdAt: number;
|
|
291
|
+
updatedAt: number;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Per-`(app, tableKey, user)` capabilities surfaced to the client so the UI
|
|
295
|
+
* can hide "Save as public" / disable "Save" before the user submits. Backed
|
|
296
|
+
* by the same hooks the write path uses, so client-side checks and
|
|
297
|
+
* server-side enforcement can never diverge.
|
|
298
|
+
*/
|
|
299
|
+
interface PresetCapabilities {
|
|
300
|
+
/** Whether the current user may set `public: true` on presets in this scope. */
|
|
301
|
+
canPublish: boolean;
|
|
302
|
+
/** Per-`(app, tableKey, user)` preset cap. Picker derives "near limit" by counting owned rows locally. */
|
|
303
|
+
presetLimit: number;
|
|
304
|
+
/**
|
|
305
|
+
* Server-known opaque identity for the current user (whatever
|
|
306
|
+
* `getCurrentUser` returns). Used by the picker / dialog to classify rows
|
|
307
|
+
* as "owned" without needing to derive it from the presets list — which
|
|
308
|
+
* fails when a user has *only* public presets (every private-row check
|
|
309
|
+
* falls back to null and own-public rows look like others-public). The
|
|
310
|
+
* controller is the single source of truth for identity, so plumbing it
|
|
311
|
+
* here also closes a class of off-by-one bugs across surfaces.
|
|
312
|
+
*/
|
|
313
|
+
userId: string;
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/presets/preset-id.d.ts
|
|
317
|
+
/** Reserved id namespace for synthetic system presets; rejected on write. */
|
|
318
|
+
declare const SYSTEM_PRESET_PREFIX = "sys:";
|
|
319
|
+
/** Deterministic id prefix for `type='userConf'` rows: `uc:${user}:${app}:${tableKey}`. */
|
|
320
|
+
declare const USER_CONF_PREFIX = "uc:";
|
|
321
|
+
/** Deterministic id prefix for `type='appConf'` rows: `ac:${user}:${app}`. */
|
|
322
|
+
declare const APP_CONF_PREFIX = "ac:";
|
|
323
|
+
/** All prefixes the server owns; client-supplied ids starting with any of these are rejected. */
|
|
324
|
+
declare const RESERVED_ID_PREFIXES: readonly ["sys:", "uc:", "ac:"];
|
|
325
|
+
/** Always-materialised system preset id; consumer may override label/content. */
|
|
326
|
+
declare const STANDARD_PRESET_ID: "sys:standard";
|
|
327
|
+
declare function userConfId(user: string, app: string, tableKey: string): string;
|
|
328
|
+
declare function appConfId(user: string, app: string): string;
|
|
329
|
+
declare function isSystemPresetId(id: string | null | undefined): boolean;
|
|
330
|
+
/**
|
|
331
|
+
* Auto-prefix a bare system-preset id (`'monitoring'` → `'sys:monitoring'`).
|
|
332
|
+
* Returns the input unchanged if it already carries the prefix.
|
|
333
|
+
*/
|
|
334
|
+
declare function normaliseSystemPresetId(id: string): string;
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/presets/system-presets.d.ts
|
|
337
|
+
/**
|
|
338
|
+
* Synthetic preset that lives only in memory — never persisted. Always
|
|
339
|
+
* present in the picker; users branch off via Save-as into a regular owned
|
|
340
|
+
* preset to evolve it.
|
|
341
|
+
*/
|
|
342
|
+
interface SystemPreset {
|
|
343
|
+
/** Canonical `sys:*` form after normalisation. */
|
|
344
|
+
id: string;
|
|
345
|
+
label: string;
|
|
346
|
+
/** Baseline snapshot. Empty `{}` falls through to factory defaults. */
|
|
347
|
+
content: PresetSnapshot;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Consumer-supplied entry. `content` is optional so a consumer can override
|
|
351
|
+
* Standard's label without supplying a snapshot.
|
|
352
|
+
*/
|
|
353
|
+
interface SystemPresetInput {
|
|
354
|
+
/** Bare ids are auto-prefixed (`'monitoring'` → `'sys:monitoring'`). */
|
|
355
|
+
id: string;
|
|
356
|
+
label: string;
|
|
357
|
+
content?: PresetSnapshot;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Resolve the consumer's `:system-presets` prop into the canonical render
|
|
361
|
+
* order: Standard at index 0 (consumer override or default empty fallback),
|
|
362
|
+
* named system presets in array order. Duplicate ids are dropped with a
|
|
363
|
+
* console.warn (first wins).
|
|
364
|
+
*/
|
|
365
|
+
declare function resolveSystemPresets(input?: SystemPresetInput[]): SystemPreset[];
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/presets/preset-dirty.d.ts
|
|
368
|
+
/**
|
|
369
|
+
* JSON-stringify with object keys sorted alphabetically at every depth so
|
|
370
|
+
* dict-shaped values (`columnWidths`, `filterOps` after deserialisation)
|
|
371
|
+
* compare structurally regardless of insertion order. Arrays preserve order.
|
|
372
|
+
*
|
|
373
|
+
* Used by the localStorage draft serializer (where a stable string is
|
|
374
|
+
* needed for storage); dirty detection prefers `isDirtyAgainst` (no
|
|
375
|
+
* stringify, short-circuits on first mismatch).
|
|
376
|
+
*/
|
|
377
|
+
declare function stableStringify(value: unknown): string;
|
|
378
|
+
/**
|
|
379
|
+
* Per-aspect dirty check: only aspects the active preset claims (key
|
|
380
|
+
* present) are compared. A column-only preset stays clean while filters
|
|
381
|
+
* change; a filter-ops-only preset doesn't dirty when columns reorder.
|
|
382
|
+
*
|
|
383
|
+
* `current` should be the full snapshot of all available aspects (i.e.
|
|
384
|
+
* `captureSnapshot()` output) so each claimed aspect on the active preset
|
|
385
|
+
* has something to compare against.
|
|
386
|
+
*/
|
|
387
|
+
declare function isDirtyAgainst(active: PresetSnapshot, current: PresetSnapshot): boolean;
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/presets/preset-draft.d.ts
|
|
390
|
+
/**
|
|
391
|
+
* localStorage overlay shape. Subset of `PresetSnapshot` — `filterOps` is
|
|
392
|
+
* intentionally excluded (filter values are situational; restoring them on
|
|
393
|
+
* mount surprises users — see PRESETS-PHASE-2 §3.8). `itemsPerPage` is
|
|
394
|
+
* gated on `availableAspects`.
|
|
395
|
+
*/
|
|
396
|
+
type PresetDraft = Omit<PresetSnapshot, "filterOps">;
|
|
397
|
+
/** Aspects eligible for local-draft persistence. `filterOps` is excluded by design. */
|
|
398
|
+
declare const DRAFT_PERSISTED_ASPECTS: readonly ["columns", "filters", "sorters", "itemsPerPage"];
|
|
399
|
+
type DraftPersistedAspect = (typeof DRAFT_PERSISTED_ASPECTS)[number];
|
|
400
|
+
/**
|
|
401
|
+
* Capture the persisted-aspect subset of a full snapshot. Aspects not in
|
|
402
|
+
* `availableAspects` are skipped (forward-compat: a deploy that toggles an
|
|
403
|
+
* aspect off doesn't error on previously-saved drafts).
|
|
404
|
+
*/
|
|
405
|
+
declare function serializeDraft(snapshot: PresetSnapshot, availableAspects: readonly PresetAspect[]): PresetDraft;
|
|
406
|
+
/**
|
|
407
|
+
* Convert a localStorage draft back to a partial snapshot suitable for
|
|
408
|
+
* `applyPreset`. Aspects no longer in `availableAspects` are silently
|
|
409
|
+
* skipped.
|
|
410
|
+
*/
|
|
411
|
+
declare function deserializeDraft(draft: PresetDraft, availableAspects: readonly PresetAspect[]): PresetSnapshot;
|
|
412
|
+
/** True when the draft has zero persisted aspects to apply. */
|
|
413
|
+
declare function isEmptyDraft(draft: PresetDraft): boolean;
|
|
414
|
+
/**
|
|
415
|
+
* True when the draft's persisted aspects exactly match the active
|
|
416
|
+
* preset's persisted aspects. Used by the watcher to decide whether to
|
|
417
|
+
* `localStorage.removeItem` instead of writing — keeps storage tidy and
|
|
418
|
+
* avoids `localStorage.length` noise after the user reverts edits.
|
|
419
|
+
*/
|
|
420
|
+
declare function draftMatchesPreset(draft: PresetDraft, presetSnapshot: PresetSnapshot, availableAspects: readonly PresetAspect[]): boolean;
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/presets/presets-client.d.ts
|
|
423
|
+
/**
|
|
424
|
+
* Configuration for a `PresetsClient`. Either pass a pre-built `client`
|
|
425
|
+
* (already wired with auth/fetch by the consumer's `ClientFactory`), or
|
|
426
|
+
* pass `clientFactory + url` so the wrapper builds one. The capabilities
|
|
427
|
+
* endpoint is fetched outside the standard CRUD path; if your app uses
|
|
428
|
+
* non-cookie auth, supply `fetch` so headers/credentials propagate.
|
|
429
|
+
*/
|
|
430
|
+
interface PresetsClientConfig {
|
|
431
|
+
/** Controller mount URL, e.g. `"/db/_presets"`. Required. */
|
|
432
|
+
url: string;
|
|
433
|
+
app: string;
|
|
434
|
+
tableKey: string;
|
|
435
|
+
/** Pre-built Client (auth-configured by the host). Wins over `clientFactory`. */
|
|
436
|
+
client?: Client;
|
|
437
|
+
/** Builds a Client for the given URL. Defaults to the app-wide `getDefaultClientFactory()`. */
|
|
438
|
+
clientFactory?: (url: string) => Client;
|
|
439
|
+
/**
|
|
440
|
+
* Fetch implementation for the `GET /capabilities` side-channel. Defaults
|
|
441
|
+
* to `globalThis.fetch`. Cookie-based auth needs no override.
|
|
442
|
+
*/
|
|
443
|
+
fetch?: typeof globalThis.fetch;
|
|
444
|
+
}
|
|
445
|
+
interface PresetsListResult {
|
|
446
|
+
/** type='preset' rows. */
|
|
447
|
+
presets: AsPresetEntryRow[];
|
|
448
|
+
/** type='userConf' row for this `(user, app, tableKey)`, or null. */
|
|
449
|
+
userConf: AsPresetEntryRow | null;
|
|
450
|
+
/**
|
|
451
|
+
* Server-issued capabilities. `null` when capabilities load failed (auth,
|
|
452
|
+
* network). `undefined` when this call skipped the capabilities fetch
|
|
453
|
+
* (refresh-after-mutation) — callers should leave their cached value
|
|
454
|
+
* untouched.
|
|
455
|
+
*/
|
|
456
|
+
capabilities: PresetCapabilities | null | undefined;
|
|
457
|
+
/** True when the controller responded 401/403 — UI silently hides. */
|
|
458
|
+
denied: boolean;
|
|
459
|
+
}
|
|
460
|
+
interface PresetsSaveAsOptions {
|
|
461
|
+
public?: boolean;
|
|
462
|
+
}
|
|
463
|
+
interface PresetsSaveResult {
|
|
464
|
+
id: string;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Framework-agnostic wrapper over `@atscript/db-client`'s `Client` for the
|
|
468
|
+
* `AsPresetEntry` table. Handles wire serialisation, list-splitting by
|
|
469
|
+
* `type`, capabilities side-channel, and 401/403 → `denied` semantics.
|
|
470
|
+
*
|
|
471
|
+
* Stateless: every method is a fresh request. The Vue composable layer
|
|
472
|
+
* holds reactive state; this class only translates intent → HTTP.
|
|
473
|
+
*/
|
|
474
|
+
declare class PresetsClient {
|
|
475
|
+
private readonly url;
|
|
476
|
+
private readonly app;
|
|
477
|
+
private readonly tableKey;
|
|
478
|
+
private readonly client;
|
|
479
|
+
private readonly fetchImpl;
|
|
480
|
+
constructor(cfg: PresetsClientConfig);
|
|
481
|
+
/**
|
|
482
|
+
* Lists owned + public preset rows AND the user's userConf row for this
|
|
483
|
+
* `(app, tableKey)`. By default also fetches `capabilities` in parallel —
|
|
484
|
+
* pass `{ capabilities: false }` for refresh-after-mutation calls (fav
|
|
485
|
+
* toggle, default change, save/save-as, public toggle, rename, delete)
|
|
486
|
+
* where role-derived capabilities can't have changed. Auth errors
|
|
487
|
+
* (401/403) collapse to `denied: true` with empty data so the UI hides
|
|
488
|
+
* itself silently.
|
|
489
|
+
*/
|
|
490
|
+
list(opts?: {
|
|
491
|
+
capabilities?: boolean;
|
|
492
|
+
}): Promise<PresetsListResult>;
|
|
493
|
+
/** GET `${url}/capabilities?app=…&tableKey=…`. Out-of-band of CRUD plumbing. */
|
|
494
|
+
loadCapabilities(): Promise<PresetCapabilities>;
|
|
495
|
+
/**
|
|
496
|
+
* Overwrite the active preset's content. Server shallow-merges `data` so
|
|
497
|
+
* `label` is preserved on the row regardless — the caller sends it
|
|
498
|
+
* anyway because the client-side validator can't pick the preset
|
|
499
|
+
* variant of the `data` union without `label` present (it's required
|
|
500
|
+
* on that variant).
|
|
501
|
+
*/
|
|
502
|
+
savePreset(id: string, label: string, snapshot: PresetSnapshot): Promise<void>;
|
|
503
|
+
/**
|
|
504
|
+
* Create a new preset row. Server stamps `user`, generates a UUID `id`,
|
|
505
|
+
* and derives `aspects` from the snapshot keys.
|
|
506
|
+
*/
|
|
507
|
+
savePresetAs(label: string, snapshot: PresetSnapshot, opts?: PresetsSaveAsOptions): Promise<PresetsSaveResult>;
|
|
508
|
+
/** Update only the label. Server re-stamps top-level `label` + `publicLabel`. */
|
|
509
|
+
renamePreset(id: string, label: string): Promise<void>;
|
|
510
|
+
/** Toggle `public`. Server re-stamps `publicLabel` accordingly. */
|
|
511
|
+
setPublic(id: string, value: boolean): Promise<void>;
|
|
512
|
+
deletePreset(id: string): Promise<void>;
|
|
513
|
+
/**
|
|
514
|
+
* Upsert the userConf row keyed on `${USER_CONF_PREFIX}${user}:${app}:${tableKey}`.
|
|
515
|
+
* Caller must know whether the row exists from a prior `list()` so we
|
|
516
|
+
* pick the right verb. Server forces `user` and `id` on insert.
|
|
517
|
+
*/
|
|
518
|
+
upsertUserConf(existing: AsPresetEntryRow | null, patch: Partial<UserConfData>, user?: string): Promise<void>;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Error raised for non-2xx responses on the capabilities side-channel.
|
|
522
|
+
* Standard CRUD errors flow through `Client`'s `ClientError`.
|
|
523
|
+
*/
|
|
524
|
+
declare class PresetsHttpError extends Error {
|
|
525
|
+
readonly status: number;
|
|
526
|
+
constructor(status: number, message: string);
|
|
527
|
+
}
|
|
528
|
+
/** True for HTTP 401/403 across both `ClientError` and `PresetsHttpError`. */
|
|
529
|
+
declare function isAuthError(err: unknown): boolean;
|
|
530
|
+
//#endregion
|
|
531
|
+
//#region src/presets/app-prefs-client.d.ts
|
|
532
|
+
/**
|
|
533
|
+
* Configuration for an `AppPrefsClient`. App-wide user prefs (`appConf`)
|
|
534
|
+
* have no `tableKey` — they're scoped per `(user, app)` only.
|
|
535
|
+
*/
|
|
536
|
+
interface AppPrefsClientConfig {
|
|
537
|
+
/** Same controller URL the presets table is mounted on. */
|
|
538
|
+
url: string;
|
|
539
|
+
app: string;
|
|
540
|
+
/** Pre-built Client (auth-configured by host). */
|
|
541
|
+
client?: Client;
|
|
542
|
+
/** Builds a Client when `client` is absent. Defaults to the app-wide `getDefaultClientFactory()`. */
|
|
543
|
+
clientFactory?: (url: string) => Client;
|
|
544
|
+
}
|
|
545
|
+
interface AppPrefsLoadResult {
|
|
546
|
+
/** Full row (server-stamped id, user, timestamps), or `null` when none exists. */
|
|
547
|
+
row: AsPresetEntryRow | null;
|
|
548
|
+
/** Convenience accessor for `row.data` (the typed prefs payload), or `null`. */
|
|
549
|
+
prefs: AppConfData | null;
|
|
550
|
+
/** True when the controller responded 401/403. */
|
|
551
|
+
denied: boolean;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Framework-agnostic wrapper for app-wide user preferences (`type='appConf'`).
|
|
555
|
+
* Independent of the preset/userConf surface — devs use this directly to
|
|
556
|
+
* read/write `appearance`, `language`, `density`, etc., without involving
|
|
557
|
+
* any table.
|
|
558
|
+
*/
|
|
559
|
+
declare class AppPrefsClient {
|
|
560
|
+
private readonly app;
|
|
561
|
+
private readonly client;
|
|
562
|
+
constructor(cfg: AppPrefsClientConfig);
|
|
563
|
+
/** Fetch the single `appConf` row for `(user, app)`. */
|
|
564
|
+
load(): Promise<AppPrefsLoadResult>;
|
|
565
|
+
/**
|
|
566
|
+
* Upsert the `appConf` row. Server forces `id` from session and shallow-
|
|
567
|
+
* merges `data` so partial patches don't wipe unrelated fields. Caller
|
|
568
|
+
* passes the prior `existing` row from `load()` so we pick the right verb.
|
|
569
|
+
*
|
|
570
|
+
* Returns the row id that owns the value after the write — useful for
|
|
571
|
+
* state tracking after the first insert (subsequent saves take the
|
|
572
|
+
* update path).
|
|
573
|
+
*/
|
|
574
|
+
save(existing: AsPresetEntryRow | null, patch: Partial<AppConfData>, user?: string): Promise<string | null>;
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/query/build-table-query.d.ts
|
|
578
|
+
/** Options for building a Uniquery from table state. */
|
|
579
|
+
interface BuildTableQueryOptions {
|
|
580
|
+
/** Paths of visible columns — used for `$select` projection. */
|
|
581
|
+
visibleColumnPaths: string[];
|
|
582
|
+
/** User-configured sorters. */
|
|
583
|
+
sorters: SortControl[];
|
|
584
|
+
/** Always-applied sorters (prepended before user sorters). */
|
|
585
|
+
forceSorters?: SortControl[];
|
|
586
|
+
/** User-configured field filters. */
|
|
587
|
+
filters: FieldFilters;
|
|
588
|
+
/** Always-applied Uniquery filter (AND'd with user filters). */
|
|
589
|
+
forceFilters?: FilterExpr;
|
|
590
|
+
/** Full-text search term. */
|
|
591
|
+
search?: string;
|
|
592
|
+
/** Search index name for `$search`. */
|
|
593
|
+
searchIndex?: string;
|
|
594
|
+
/**
|
|
595
|
+
* Set `controls.$actions = true` so each returned row carries
|
|
596
|
+
* `$actions: string[]` — server-evaluated names of row/rows-level actions
|
|
597
|
+
* NOT disabled for that row. Off by default; renderers flip it on when a
|
|
598
|
+
* row-actions column will render gateable actions.
|
|
599
|
+
*/
|
|
600
|
+
includeActions?: boolean;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Build a Uniquery object from table UI state.
|
|
604
|
+
*
|
|
605
|
+
* Pure function — no framework dependencies.
|
|
606
|
+
* Combines user filters with force filters, merges sorters,
|
|
607
|
+
* projects visible columns, and applies pagination.
|
|
608
|
+
*/
|
|
609
|
+
declare function buildTableQuery(opts: BuildTableQueryOptions): Uniquery;
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/query/merge-sorters.d.ts
|
|
612
|
+
/**
|
|
613
|
+
* Merge force sorters with user sorters.
|
|
614
|
+
*
|
|
615
|
+
* Force sorters come first and take priority — if a field appears
|
|
616
|
+
* in both lists, the force entry wins and the user entry is dropped.
|
|
617
|
+
*/
|
|
618
|
+
declare function mergeSorters(forceSorters: SortControl[], userSorters: SortControl[]): SortControl[];
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/query/merge-filters.d.ts
|
|
621
|
+
/**
|
|
622
|
+
* AND-merge two filter expressions, producing a wire shape that survives
|
|
623
|
+
* `@uniqu/url`'s `mergeConjunction` parser collapse.
|
|
624
|
+
*
|
|
625
|
+
* The collapse problem: when two `$and` siblings target the same field
|
|
626
|
+
* with the same op (e.g. `{status: 'cancelled'}` AND `{status: 'shipped'}`),
|
|
627
|
+
* the parser silently merges them on the receiving end and one clause is
|
|
628
|
+
* dropped. This would let a colliding user-side filter erase a
|
|
629
|
+
* `forceFilters` clause, breaking that contract.
|
|
630
|
+
*
|
|
631
|
+
* Fix: detect same-field same-op collisions and wrap each repeat in
|
|
632
|
+
* `$not({$not: ...})`. `$not` nodes are preserved verbatim by the parser,
|
|
633
|
+
* and `!!p ≡ p` is a semantic identity, so the server evaluator sees the
|
|
634
|
+
* same AND. Non-colliding merges produce the canonical `$and` shape.
|
|
635
|
+
*/
|
|
636
|
+
declare function mergeFilters(a: FilterExpr | undefined, b: FilterExpr | undefined): FilterExpr | undefined;
|
|
637
|
+
//#endregion
|
|
638
|
+
//#region src/query/url-query.d.ts
|
|
639
|
+
/** State subset that round-trips through the URL bridge. */
|
|
640
|
+
interface UrlQueryStateLike {
|
|
641
|
+
filters: FieldFilters;
|
|
642
|
+
sorters: SortControl[];
|
|
643
|
+
/** 1-based page number. `1` is the default and is omitted from the URL. */
|
|
644
|
+
page?: number;
|
|
645
|
+
/** Per-page size. Omitted from the URL when equal to `defaultItemsPerPage`. */
|
|
646
|
+
itemsPerPage?: number;
|
|
647
|
+
/** Full-text search term. Omitted from the URL when empty. */
|
|
648
|
+
searchTerm?: string;
|
|
649
|
+
}
|
|
650
|
+
/** Snapshot recovered from a URL string — partial on purpose so callers can layer it onto state. */
|
|
651
|
+
interface UrlQueryStateSnapshot {
|
|
652
|
+
filters: FieldFilters;
|
|
653
|
+
sorters: SortControl[];
|
|
654
|
+
/**
|
|
655
|
+
* Raw record offset from `$skip` (omitted when no `$skip` in URL). The
|
|
656
|
+
* decoder does NOT compute a page index — that requires `itemsPerPage`,
|
|
657
|
+
* which is the consumer's private preference. Consumers compute
|
|
658
|
+
* `page = Math.floor(skip / currentItemsPerPage) + 1`.
|
|
659
|
+
*/
|
|
660
|
+
skip?: number;
|
|
661
|
+
searchTerm: string;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Per-aspect opt-in/out for the URL bridge. Honoured symmetrically by both
|
|
665
|
+
* `stateToUrlQueryString` (encoder) and `urlQueryStringToState` (decoder) —
|
|
666
|
+
* the same gate must apply to both directions or the `lastEmittedUrl`
|
|
667
|
+
* echo guard mismatches and produces self-echoing URLs.
|
|
668
|
+
*
|
|
669
|
+
* Default (omitted, or any field `undefined` / `true`): full sync — backward
|
|
670
|
+
* compatible with pre-`UrlQuerySync` behaviour.
|
|
671
|
+
*/
|
|
672
|
+
interface UrlQuerySync {
|
|
673
|
+
/**
|
|
674
|
+
* Filters round-trip.
|
|
675
|
+
* - `true` / `undefined` (default): all filters.
|
|
676
|
+
* - `false` / `[]`: no filters in URL; `applyUrlQuery` skips filter writes.
|
|
677
|
+
* - `string[]`: only listed field paths; non-allowlist filters stay private.
|
|
678
|
+
*/
|
|
679
|
+
filters?: boolean | string[];
|
|
680
|
+
/** Sorters — same `boolean | string[]` semantics, allowlist matches `SortControl.field`. */
|
|
681
|
+
sorters?: boolean | string[];
|
|
682
|
+
/** Whether `searchTerm` syncs as `$search`. Default true. */
|
|
683
|
+
search?: boolean;
|
|
684
|
+
/** Whether pagination (`$skip` + `$limit`) syncs. Page and limit are coupled — one knob. */
|
|
685
|
+
pagination?: boolean;
|
|
686
|
+
}
|
|
687
|
+
interface UrlQueryDefaults {
|
|
688
|
+
/** Consumer's `:limit` prop. Used to omit `$limit` from the URL when state matches it. */
|
|
689
|
+
defaultItemsPerPage: number;
|
|
690
|
+
/** Per-aspect sync gates. Omitted = full sync (existing behaviour). */
|
|
691
|
+
sync?: UrlQuerySync;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Resolve a `boolean | string[]` aspect gate into a tri-state:
|
|
695
|
+
* - `"all"` → pass through unchanged
|
|
696
|
+
* - `"none"` → empty / off
|
|
697
|
+
* - `Set<string>` → allowlist
|
|
698
|
+
*/
|
|
699
|
+
type AspectGate = "all" | "none" | Set<string>;
|
|
700
|
+
declare function resolveAspectGate(value: boolean | string[] | undefined): AspectGate;
|
|
701
|
+
/**
|
|
702
|
+
* Serialize the table state subset into a URL query string.
|
|
703
|
+
*
|
|
704
|
+
* Reuses `buildTableQuery` for the filter/sort/search shape (no `$select`,
|
|
705
|
+
* `$actions`, `forceFilters`, `forceSorters` — those are not user state) and
|
|
706
|
+
* appends `$skip` / `$limit` for pagination.
|
|
707
|
+
*
|
|
708
|
+
* Returns `""` (no leading `?`) for the default view.
|
|
709
|
+
*/
|
|
710
|
+
declare function stateToUrlQueryString(state: UrlQueryStateLike, defaults: UrlQueryDefaults): string;
|
|
711
|
+
interface UrlQueryParseOptions {
|
|
712
|
+
/**
|
|
713
|
+
* Field paths the table knows about. Conditions on fields outside this set
|
|
714
|
+
* are silently dropped. Omit to accept any field (useful when the table
|
|
715
|
+
* definition isn't loaded yet).
|
|
716
|
+
*/
|
|
717
|
+
knownFields?: Iterable<string>;
|
|
718
|
+
/** Per-aspect sync gates — must match the encoder's config to keep the round-trip symmetric. */
|
|
719
|
+
sync?: UrlQuerySync;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Parse a URL query string back into the table state subset.
|
|
723
|
+
*
|
|
724
|
+
* Robust by design — schema drift and copy-paste errors must not break the
|
|
725
|
+
* recipient's view:
|
|
726
|
+
* - unknown fields (not in `knownFields`) → silently dropped
|
|
727
|
+
* - unsupported operators → silently dropped
|
|
728
|
+
* - unknown controls (e.g. `$weird=42`) → silently ignored
|
|
729
|
+
* - malformed query → `{ filters: {}, sorters: [], searchTerm: "" }`
|
|
730
|
+
*
|
|
731
|
+
* `page` and `itemsPerPage` are returned only when the URL specified them
|
|
732
|
+
* (`$skip` / `$limit`); callers compose them onto state without overwriting
|
|
733
|
+
* defaults when the URL was silent.
|
|
734
|
+
*/
|
|
735
|
+
declare function urlQueryStringToState(urlString: string, opts?: UrlQueryParseOptions): UrlQueryStateSnapshot;
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/selection/selection-fns.d.ts
|
|
738
|
+
/** Selection mode. */
|
|
739
|
+
type SelectionMode = "none" | "single" | "multi";
|
|
740
|
+
/**
|
|
741
|
+
* Toggle a single PK in the selection.
|
|
742
|
+
*
|
|
743
|
+
* - `"none"`: no-op (returns the same array reference).
|
|
744
|
+
* - `"single"`: replaces the selection. Toggling the already-selected PK clears it.
|
|
745
|
+
* - `"multi"`: adds when absent, removes when present. Order preserved on add (append).
|
|
746
|
+
*
|
|
747
|
+
* Always returns a new array on actual mutation; returns the same reference for `"none"`.
|
|
748
|
+
*/
|
|
749
|
+
declare function togglePk(selection: readonly unknown[], pk: unknown, mode: SelectionMode): unknown[];
|
|
750
|
+
/**
|
|
751
|
+
* Drop every PK in `selection` that's not in `presentPks`.
|
|
752
|
+
*
|
|
753
|
+
* Returns the same `selection` reference when nothing was dropped, so callers
|
|
754
|
+
* can rely on identity comparison to detect a no-op without an explicit
|
|
755
|
+
* length check.
|
|
756
|
+
*/
|
|
757
|
+
declare function trimSelection(selection: readonly unknown[], presentPks: ReadonlySet<unknown>): unknown[];
|
|
758
|
+
/** Map rows to their PKs via `rowValueFn`. */
|
|
759
|
+
declare function rowsToPks(rows: readonly Record<string, unknown>[], rowValueFn: (row: Record<string, unknown>) => unknown): unknown[];
|
|
760
|
+
//#endregion
|
|
761
|
+
//#region src/columns/column-widths.d.ts
|
|
762
|
+
/** Per-column width entry: `w` is the rendered width, `d` is the default. */
|
|
763
|
+
interface ColumnWidthEntry {
|
|
764
|
+
w: string;
|
|
765
|
+
d: string;
|
|
766
|
+
}
|
|
767
|
+
/** Map keyed by `ColumnDef.path`. Always fully populated for every column. */
|
|
768
|
+
type ColumnWidthsMap = Record<string, ColumnWidthEntry>;
|
|
769
|
+
/** Upper bound applied only to default-width computation. Annotation widths and manual resize are not capped. */
|
|
770
|
+
declare const MAX_DEFAULT_COLUMN_WIDTH_PX = 320;
|
|
771
|
+
/**
|
|
772
|
+
* Computes a sane default width for a column from its type + length annotation.
|
|
773
|
+
*
|
|
774
|
+
* Resolution order:
|
|
775
|
+
* 1. col.width (`@ui.table.width` annotation) wins outright.
|
|
776
|
+
* 2. Otherwise pick from a type-based table; for `text` columns with `maxLen`,
|
|
777
|
+
* derive from char-count using an 8px-per-char heuristic.
|
|
778
|
+
* 3. Cap the result at MAX_DEFAULT_COLUMN_WIDTH_PX (does not apply to step 1).
|
|
779
|
+
*/
|
|
780
|
+
declare function computeDefaultColumnWidth(col: ColumnDef): string;
|
|
781
|
+
/**
|
|
782
|
+
* Reconciles a current ColumnWidthsMap against the latest column list:
|
|
783
|
+
* - new columns get a fresh `{ w: d, d }` entry where `d = computeDefaultColumnWidth(col)`
|
|
784
|
+
* - existing columns keep `w` (preserves manual resize) but refresh `d`
|
|
785
|
+
* - paths no longer present are kept (preserves user widths if columns reappear)
|
|
786
|
+
*
|
|
787
|
+
* Returns a NEW map only when something actually changed; otherwise returns `current`
|
|
788
|
+
* so a shallowRef writer can short-circuit.
|
|
789
|
+
*/
|
|
790
|
+
declare function reconcileColumnWidthDefaults(allColumns: ColumnDef[], current: ColumnWidthsMap): ColumnWidthsMap;
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region src/state/table-state-types.d.ts
|
|
793
|
+
type ConfigTab = "columns" | "filters" | "sorters";
|
|
794
|
+
/**
|
|
795
|
+
* Framework-agnostic table state data.
|
|
796
|
+
*
|
|
797
|
+
* Defines the contract that any framework wrapper (Vue/React) must implement.
|
|
798
|
+
* Values are plain — the framework wraps them in its own reactive primitives.
|
|
799
|
+
*/
|
|
800
|
+
interface TableStateData {
|
|
801
|
+
/** Resolved table definition (null until metadata loads). */
|
|
802
|
+
tableDef: TableDef | null;
|
|
803
|
+
/** True while the table metadata (TableDef) is being loaded. */
|
|
804
|
+
loadingMetadata: boolean;
|
|
805
|
+
/** Names of visible columns, in display order. */
|
|
806
|
+
columnNames: string[];
|
|
807
|
+
/** Current visible columns (derived from columnNames + allColumns). */
|
|
808
|
+
columns: ColumnDef[];
|
|
809
|
+
/** All columns from tableDef (including hidden). */
|
|
810
|
+
allColumns: ColumnDef[];
|
|
811
|
+
/**
|
|
812
|
+
* Per-column widths keyed by column path. Always fully populated for every
|
|
813
|
+
* column in `allColumns` once the TableDef has loaded. Each entry carries
|
|
814
|
+
* the current rendered width (`w`) and the default (`d`) it would reset to.
|
|
815
|
+
* Default sourcing: `@ui.table.width` annotation > type+@expect.maxLen-derived.
|
|
816
|
+
*/
|
|
817
|
+
columnWidths: ColumnWidthsMap;
|
|
818
|
+
/** Names of filter fields displayed in the filter bar. */
|
|
819
|
+
filterFields: string[];
|
|
820
|
+
/** Active field filters. */
|
|
821
|
+
filters: FieldFilters;
|
|
822
|
+
/** Active sorters. */
|
|
823
|
+
sorters: SortControl[];
|
|
824
|
+
/**
|
|
825
|
+
* Rows of the contiguous "results island" anchored at `resultsStart`. This
|
|
826
|
+
* is a real array (not a computed view) — same row references also live in
|
|
827
|
+
* `windowCache` at their absolute indices. Writes flow through both, so
|
|
828
|
+
* extending `results` automatically updates the cache.
|
|
829
|
+
*/
|
|
830
|
+
results: Record<string, unknown>[];
|
|
831
|
+
/**
|
|
832
|
+
* Absolute row index where `results[0]` sits. Reset by `query()` and
|
|
833
|
+
* `invalidate()` from `pagination.page`; shifted by backward merges in
|
|
834
|
+
* `loadRange`'s digest.
|
|
835
|
+
*/
|
|
836
|
+
resultsStart: number;
|
|
837
|
+
/**
|
|
838
|
+
* Universal storage for every loaded row keyed by absolute index. Includes
|
|
839
|
+
* the rows in `results` (same row references, two indexings).
|
|
840
|
+
*/
|
|
841
|
+
windowCache: Map<number, Record<string, unknown>>;
|
|
842
|
+
/**
|
|
843
|
+
* Block firstIndex values currently being fetched by `loadRange`. A row at
|
|
844
|
+
* `absIdx` is in flight when its block (`floor(absIdx / blockSize) * blockSize`)
|
|
845
|
+
* is in this set. Drives the per-row skeleton placeholder in window-mode
|
|
846
|
+
* rendering. NOT set by `query()` (uses `querying`) or `queryNext()` (uses
|
|
847
|
+
* `queryingNext`).
|
|
848
|
+
*/
|
|
849
|
+
windowLoading: Set<number>;
|
|
850
|
+
/** Absolute row index at the top of a windowed renderer's visible viewport. */
|
|
851
|
+
topIndex: number;
|
|
852
|
+
/** Number of fixed-pool rows a windowed renderer is currently displaying. */
|
|
853
|
+
viewportRowCount: number;
|
|
854
|
+
/** Whether a query is currently in flight. */
|
|
855
|
+
querying: boolean;
|
|
856
|
+
/** Whether a "load more" query is in flight. */
|
|
857
|
+
queryingNext: boolean;
|
|
858
|
+
/** Total row count from the server. */
|
|
859
|
+
totalCount: number;
|
|
860
|
+
/** Number of rows currently loaded. */
|
|
861
|
+
loadedCount: number;
|
|
862
|
+
/** Pagination state. */
|
|
863
|
+
pagination: PaginationControl;
|
|
864
|
+
/** Last query error (null when successful). */
|
|
865
|
+
queryError: Error | null;
|
|
866
|
+
/** Metadata loading error (null when successful). */
|
|
867
|
+
metadataError: Error | null;
|
|
868
|
+
/** True when state changed during an in-flight query (results are stale). */
|
|
869
|
+
mustRefresh: boolean;
|
|
870
|
+
/** Full-text search term. */
|
|
871
|
+
searchTerm: string;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Methods that any table state implementation must provide.
|
|
875
|
+
*
|
|
876
|
+
* The table is model-driven: these mutators are thin wrappers that each touch
|
|
877
|
+
* exactly one entity (`filterFields` OR `filters`, never both). Side effects
|
|
878
|
+
* like pagination reset or re-query happen in watchers on the root state arrays,
|
|
879
|
+
* not inside the mutators — so any writer (dialog, external v-model, custom
|
|
880
|
+
* toolbar) gets identical behaviour.
|
|
881
|
+
*/
|
|
882
|
+
interface TableStateMethods {
|
|
883
|
+
/**
|
|
884
|
+
* Microtask-coalesced refresh. Synchronously sets `querying = true` so
|
|
885
|
+
* consumers see the loading state immediately, then schedules the actual
|
|
886
|
+
* fetch on the next microtask. Multiple `query()` calls and watcher-driven
|
|
887
|
+
* scheduleQuery calls in the same synchronous block coalesce into one
|
|
888
|
+
* fetch. Use this from dialogs / mutators where you don't need to await.
|
|
889
|
+
*/
|
|
890
|
+
query(): void;
|
|
891
|
+
/**
|
|
892
|
+
* Synchronous refresh — fires the fetch now and returns a Promise that
|
|
893
|
+
* settles when the response (or error) has been processed. Cancels any
|
|
894
|
+
* pending coalesced query so we don't double-fire. Honours `blockQuery`,
|
|
895
|
+
* `forceFilters`, `forceSorters`, `queryFn`. Use this from refresh
|
|
896
|
+
* buttons / programmatic flows that need the result.
|
|
897
|
+
*/
|
|
898
|
+
queryImmediate(): Promise<void>;
|
|
899
|
+
/**
|
|
900
|
+
* Append-style extension. Does NOT mutate `pagination.page` (extension
|
|
901
|
+
* runs parallel to pagination). Re-entry guarded via `queryingNext`.
|
|
902
|
+
*/
|
|
903
|
+
queryNext(): void;
|
|
904
|
+
/**
|
|
905
|
+
* Page-aligned slice fetch into `windowCache` with merge-on-contiguous.
|
|
906
|
+
* Returns a Promise that settles when ALL dispatched blocks have settled
|
|
907
|
+
* (success, error, OR generation-discarded); never rejects. Resolves on
|
|
908
|
+
* the next microtask when zero blocks need dispatching.
|
|
909
|
+
*/
|
|
910
|
+
loadRange(skip: number, limit: number): Promise<void>;
|
|
911
|
+
/**
|
|
912
|
+
* Explicit immediate wipe — clears results / cache / loading, resets
|
|
913
|
+
* `resultsStart` from `pagination.page`, sets `totalCount = 0`. Does NOT
|
|
914
|
+
* auto-refetch.
|
|
915
|
+
*/
|
|
916
|
+
invalidate(): void;
|
|
917
|
+
/** O(1) read of `windowCache.get(absIdx)`. */
|
|
918
|
+
dataAt(absIdx: number): Record<string, unknown> | undefined;
|
|
919
|
+
/** True if the block covering `absIdx` is currently being fetched. */
|
|
920
|
+
loadingAt(absIdx: number): boolean;
|
|
921
|
+
/** Returns the last error attached to the block covering `absIdx`, or null. */
|
|
922
|
+
errorAt(absIdx: number): Error | null;
|
|
923
|
+
/** Clear all applied filters. Does not touch `filterFields`. */
|
|
924
|
+
resetFilters(): void;
|
|
925
|
+
/** Open the config dialog (optionally to a specific tab). */
|
|
926
|
+
showConfigDialog(tab?: ConfigTab): void;
|
|
927
|
+
/** Append a field to the displayed filter fields (deduped). */
|
|
928
|
+
addFilterField(path: string): void;
|
|
929
|
+
/**
|
|
930
|
+
* Remove a field from the displayed filter fields. Does NOT clear the
|
|
931
|
+
* applied filter value — display state and applied state are independent.
|
|
932
|
+
* Use `removeFieldFilter` to clear the value.
|
|
933
|
+
*/
|
|
934
|
+
removeFilterField(path: string): void;
|
|
935
|
+
/** Set applied filter conditions for a field. Does not touch `filterFields`. */
|
|
936
|
+
setFieldFilter(path: string, conditions: FilterCondition[]): void;
|
|
937
|
+
/**
|
|
938
|
+
* Set the rendered width for a column. No-op if the column is unknown or
|
|
939
|
+
* the value is unchanged. Does not modify the column's default (`d`).
|
|
940
|
+
*/
|
|
941
|
+
setColumnWidth(path: string, width: string): void;
|
|
942
|
+
/**
|
|
943
|
+
* Reset the rendered width back to the column's default (`d`). No-op if
|
|
944
|
+
* the column is unknown or already at default.
|
|
945
|
+
*/
|
|
946
|
+
resetColumnWidth(path: string): void;
|
|
947
|
+
/**
|
|
948
|
+
* Clear the applied filter for a field. Does NOT remove the field from
|
|
949
|
+
* `filterFields` — the input row stays visible.
|
|
950
|
+
*/
|
|
951
|
+
removeFieldFilter(path: string): void;
|
|
952
|
+
/** Open filter dialog for a column. */
|
|
953
|
+
openFilterDialog(column: ColumnDef): void;
|
|
954
|
+
/** Close filter dialog. */
|
|
955
|
+
closeFilterDialog(): void;
|
|
956
|
+
}
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/state/tokens.d.ts
|
|
959
|
+
declare const DEFAULT_ROW_HEIGHT_PX = 32;
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region src/state/window/page-aligned-blocks.d.ts
|
|
962
|
+
/**
|
|
963
|
+
* Clamp a window's `topIndex` to the valid `[0, totalCount - viewport]` range.
|
|
964
|
+
* `viewport` past `totalCount` (or zero/negative totals) collapses to `0`.
|
|
965
|
+
*/
|
|
966
|
+
declare function clampTopIndex(topIndex: number, totalCount: number, viewport: number): number;
|
|
967
|
+
/** Index of the first row in the block that contains `absIdx`. */
|
|
968
|
+
declare function blockStartFor(absIdx: number, blockSize: number): number;
|
|
969
|
+
interface PageAlignedBlock {
|
|
970
|
+
page: number;
|
|
971
|
+
firstIndex: number;
|
|
972
|
+
}
|
|
973
|
+
declare function pageAlignedBlocksFor(skip: number, limit: number, blockSize: number): PageAlignedBlock[];
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region src/state/window/results-merge.d.ts
|
|
976
|
+
type Row = Record<string, unknown>;
|
|
977
|
+
interface MergeResult {
|
|
978
|
+
newResults: Row[];
|
|
979
|
+
newResultsStart: number;
|
|
980
|
+
}
|
|
981
|
+
declare function walkForwardAbsorb(results: Row[], resultsStart: number, cache: Map<number, Row>): MergeResult;
|
|
982
|
+
declare function walkBackwardAbsorb(results: Row[], resultsStart: number, cache: Map<number, Row>): MergeResult;
|
|
983
|
+
//#endregion
|
|
984
|
+
//#region src/state/window/plan-fetch.d.ts
|
|
985
|
+
type FetchPlanMode = "jump" | "steady";
|
|
986
|
+
interface FetchPlan {
|
|
987
|
+
skip: number;
|
|
988
|
+
limit: number;
|
|
989
|
+
mode: FetchPlanMode;
|
|
990
|
+
}
|
|
991
|
+
interface PlanFetchArgs {
|
|
992
|
+
top: number;
|
|
993
|
+
viewport: number;
|
|
994
|
+
totalCount: number;
|
|
995
|
+
cache: Map<number, unknown>;
|
|
996
|
+
blockSize: number;
|
|
997
|
+
/** Threshold below which a steady prefetch fires. Typically `blockSize / 4`. */
|
|
998
|
+
buffer: number;
|
|
999
|
+
}
|
|
1000
|
+
declare function planFetch(args: PlanFetchArgs): FetchPlan | null;
|
|
1001
|
+
//#endregion
|
|
1002
|
+
//#region src/utils/debounce.d.ts
|
|
1003
|
+
/**
|
|
1004
|
+
* Creates a debounced version of a function that delays invocation
|
|
1005
|
+
* until `ms` milliseconds have elapsed since the last call.
|
|
1006
|
+
*/
|
|
1007
|
+
declare function debounce<T extends (...args: unknown[]) => void>(fn: T, ms: number): T & {
|
|
1008
|
+
cancel(): void;
|
|
1009
|
+
};
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/utils/equality.d.ts
|
|
1012
|
+
declare function arraysEqual<T>(a: readonly T[], b: readonly T[]): boolean;
|
|
1013
|
+
declare function sameColumnSet<T>(a: readonly T[], b: readonly T[]): boolean;
|
|
1014
|
+
declare function setsEqual<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean;
|
|
1015
|
+
declare function sortersEqual(a: SortControl[], b: SortControl[]): boolean;
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region src/utils/reorder-column-names.d.ts
|
|
1018
|
+
type ColumnReorderPosition = "before" | "after";
|
|
1019
|
+
/**
|
|
1020
|
+
* Permute a column-names array: move `fromPath` next to `toPath` at the
|
|
1021
|
+
* resolved insertion side. Pure: returns a new array, never mutates input.
|
|
1022
|
+
*
|
|
1023
|
+
* Returns the input array unchanged when:
|
|
1024
|
+
* - `fromPath === toPath`
|
|
1025
|
+
* - either path is missing from `names`
|
|
1026
|
+
* - the resolved insertion index leaves the array identical
|
|
1027
|
+
* (e.g. `[a,b,c]` from=`b` to=`a` "after" — `b` is already after `a`).
|
|
1028
|
+
*
|
|
1029
|
+
* The insertion index is resolved against post-removal positions to keep
|
|
1030
|
+
* the math symmetric across left-to-right and right-to-left moves.
|
|
1031
|
+
*/
|
|
1032
|
+
declare function reorderColumnNames(names: string[], fromPath: string, toPath: string, position: ColumnReorderPosition): string[];
|
|
1033
|
+
//#endregion
|
|
1034
|
+
export { APP_CONF_PREFIX, type AppConfData, AppPrefsClient, type AppPrefsClientConfig, type AppPrefsLoadResult, type AsPresetEntryData, type AsPresetEntryRow, type AsPresetsErrorCode, type AspectGate, type AspectMask, type BuildTableQueryOptions, type ColumnFilterType, type ColumnReorderPosition, type ColumnWidthEntry, type ColumnWidthsMap, type ConfigTab, DEFAULT_ROW_HEIGHT_PX, DRAFT_PERSISTED_ASPECTS, type DateShortcut, type DraftPersistedAspect, type FetchPlan, type FetchPlanMode, type FieldFilters, type FilterCondition, type FilterConditionType, MAX_DEFAULT_COLUMN_WIDTH_PX, type MergeResult, NULL_OPS, PRESET_ASPECTS, type PageAlignedBlock, type PlanFetchArgs, type PresetAspect, type PresetCapabilities, type PresetColumnWidthEntry, type PresetData, type PresetDraft, type PresetFilterOpEntry, type PresetLimitReachedBody, type PresetSnapshot, type PresetSnapshotWire, type PresetSorterEntry, PresetsClient, type PresetsClientConfig, PresetsHttpError, type PresetsListResult, type PresetsSaveAsOptions, type PresetsSaveResult, RESERVED_ID_PREFIXES, STANDARD_PRESET_ID, SYSTEM_PRESET_PREFIX, type SelectionMode, type SystemPreset, type SystemPresetInput, type TableStateData, type TableStateMethods, USER_CONF_PREFIX, type UrlQueryDefaults, type UrlQueryParseOptions, type UrlQueryStateLike, type UrlQueryStateSnapshot, type UrlQuerySync, type UserConfData, appConfId, arraysEqual, blockStartFor, buildTableQuery, clampTopIndex, columnFilterType, computeDefaultColumnWidth, conditionLabel, conditionsForType, dateShortcuts, debounce, defaultCondition, derivePresetAspects, deserializeDraft, draftMatchesPreset, escapeRegex, filledFilterCount, filterTokenLabel, filtersToUniqueryFilter, formatFilterCondition, fromWireSnapshot, hasSecondValue, isAuthError, isDirtyAgainst, isEmptyDraft, isFilled, isSimpleEq, isSystemPresetId, mergeFilters, mergeSorters, normaliseSystemPresetId, pageAlignedBlocksFor, parseFilterInput, planFetch, reconcileColumnWidthDefaults, reorderColumnNames, resolveAspectGate, resolveSystemPresets, rowsToPks, sameColumnSet, serializeDraft, setsEqual, sortersEqual, stableStringify, stateToUrlQueryString, toWireSnapshot, togglePk, trimSelection, unescapeRegex, uniqueryFilterToFieldFilters, urlQueryStringToState, userConfId, walkBackwardAbsorb, walkForwardAbsorb };
|