@pythia-software/query-table-react 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,706 @@
1
+ // src/useQueryFilters.ts
2
+ import { useCallback, useMemo } from "react";
3
+ import {
4
+ isOrGroup,
5
+ predicatesOf,
6
+ negateClause,
7
+ opsForField
8
+ } from "@pythia-software/query-table-core";
9
+ function useQueryFilters(schema, setQuery) {
10
+ const byName = useMemo(
11
+ () => new Map(schema.fields.map((field) => [field.name, field])),
12
+ [schema]
13
+ );
14
+ const addFilter = useCallback(
15
+ (clause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })),
16
+ [setQuery]
17
+ );
18
+ const updateFilter = useCallback(
19
+ (index, clause) => setQuery((q) => ({
20
+ ...q,
21
+ offset: 0,
22
+ where: q.where.map((t, i) => i === index ? clause : t)
23
+ })),
24
+ [setQuery]
25
+ );
26
+ const removeFilter = useCallback(
27
+ (index) => setQuery((q) => ({
28
+ ...q,
29
+ offset: 0,
30
+ where: q.where.filter((_, i) => i !== index)
31
+ })),
32
+ [setQuery]
33
+ );
34
+ const clearFilters = useCallback(
35
+ () => setQuery((q) => ({ ...q, offset: 0, where: [] })),
36
+ [setQuery]
37
+ );
38
+ const updatePredicate = useCallback(
39
+ (termIndex, predIndex, clause) => setQuery((q) => ({
40
+ ...q,
41
+ offset: 0,
42
+ where: q.where.map((term, i) => {
43
+ if (i !== termIndex) return term;
44
+ if (isOrGroup(term))
45
+ return {
46
+ any: term.any.map((c, j) => j === predIndex ? clause : c)
47
+ };
48
+ return clause;
49
+ })
50
+ })),
51
+ [setQuery]
52
+ );
53
+ const removePredicate = useCallback(
54
+ (termIndex, predIndex) => setQuery((q) => {
55
+ const where = [];
56
+ q.where.forEach((term, i) => {
57
+ if (i !== termIndex) {
58
+ where.push(term);
59
+ return;
60
+ }
61
+ if (isOrGroup(term)) {
62
+ const members = term.any.filter((_, j) => j !== predIndex);
63
+ if (members.length === 1) where.push(members[0]);
64
+ else if (members.length > 1) where.push({ any: members });
65
+ return;
66
+ }
67
+ });
68
+ return { ...q, offset: 0, where };
69
+ }),
70
+ [setQuery]
71
+ );
72
+ const negateOne = useCallback(
73
+ (clause) => {
74
+ const field = byName.get(clause.field);
75
+ return negateClause(clause, field ? opsForField(field) : void 0);
76
+ },
77
+ [byName]
78
+ );
79
+ const negatePredicate = useCallback(
80
+ (termIndex, predIndex) => setQuery((q) => ({
81
+ ...q,
82
+ offset: 0,
83
+ where: q.where.map((term, i) => {
84
+ if (i !== termIndex) return term;
85
+ if (isOrGroup(term))
86
+ return {
87
+ any: term.any.map((c, j) => j === predIndex ? negateOne(c) : c)
88
+ };
89
+ return negateOne(term);
90
+ })
91
+ })),
92
+ [setQuery, negateOne]
93
+ );
94
+ const reorderFilters = useCallback(
95
+ (fromIndex, toIndex) => setQuery((q) => {
96
+ if (fromIndex < 0 || fromIndex >= q.where.length) return q;
97
+ const where = [...q.where];
98
+ const [moved] = where.splice(fromIndex, 1);
99
+ where.splice(Math.max(0, Math.min(toIndex, where.length)), 0, moved);
100
+ return { ...q, offset: 0, where };
101
+ }),
102
+ [setQuery]
103
+ );
104
+ const mergeFilters = useCallback(
105
+ (sourceIndex, targetIndex) => setQuery((q) => {
106
+ if (sourceIndex === targetIndex) return q;
107
+ const source = q.where[sourceIndex];
108
+ const target = q.where[targetIndex];
109
+ if (!source || !target) return q;
110
+ const merged = {
111
+ any: [...predicatesOf(target), ...predicatesOf(source)]
112
+ };
113
+ const where = [];
114
+ q.where.forEach((term, i) => {
115
+ if (i === sourceIndex) return;
116
+ where.push(i === targetIndex ? merged : term);
117
+ });
118
+ return { ...q, offset: 0, where };
119
+ }),
120
+ [setQuery]
121
+ );
122
+ return {
123
+ addFilter,
124
+ updateFilter,
125
+ removeFilter,
126
+ clearFilters,
127
+ updatePredicate,
128
+ removePredicate,
129
+ negatePredicate,
130
+ reorderFilters,
131
+ mergeFilters
132
+ };
133
+ }
134
+
135
+ // src/useGrouping.ts
136
+ import { useEffect, useRef, useState } from "react";
137
+ import {
138
+ MAX_SUMMARY_COLUMNS,
139
+ normalizeQueryState,
140
+ predicatesOf as predicatesOf2,
141
+ withoutGrouping,
142
+ queriesEqual
143
+ } from "@pythia-software/query-table-core";
144
+ function useGroupingController(api, source, model, inputs, supported, viewRevision, execution = {}) {
145
+ const [notice, setNotice] = useState("");
146
+ useEffect(() => setNotice(""), [viewRevision]);
147
+ const previous = useRef(null);
148
+ const stage = api.query.grouping;
149
+ const view = {
150
+ version: 2,
151
+ mode: stage ? "groups" : "rows",
152
+ keys: stage?.keys ?? [],
153
+ summaries: stage?.summaries ?? [],
154
+ representative: stage?.representative ?? null,
155
+ input: source.query,
156
+ result: withoutGrouping(api.query)
157
+ };
158
+ const outputSchema = {
159
+ ...api.schema,
160
+ fields: api.computed.catalogue
161
+ };
162
+ function changeKeys(keys) {
163
+ if (keys.length && !supported) return;
164
+ const cached = previous.current?.revision === viewRevision ? previous.current.query : null;
165
+ const canRestore = (current) => !current.grouping && cached?.grouping && queriesEqual(cached.grouping.source, current) && JSON.stringify(cached.grouping.keys) === JSON.stringify(keys);
166
+ if (keys.length)
167
+ model.validate(canRestore(api.query) ? cached.grouping : {
168
+ ...stage ?? { summaries: [], representative: null },
169
+ keys
170
+ });
171
+ if (!keys.length && stage)
172
+ previous.current = { query: api.query, revision: viewRevision };
173
+ api.setQuery((current) => {
174
+ const before = current.grouping;
175
+ if (!keys.length) return before ? before.source : current;
176
+ if (!before) {
177
+ if (cached?.grouping && canRestore(current))
178
+ return {
179
+ ...cached,
180
+ grouping: { ...cached.grouping, source: current }
181
+ };
182
+ const grouping2 = {
183
+ keys,
184
+ summaries: [],
185
+ representative: null,
186
+ source: current
187
+ };
188
+ return {
189
+ select: keys.map((field) => ({ field })),
190
+ where: [],
191
+ orderBy: [],
192
+ limit: current.limit,
193
+ offset: 0,
194
+ grouping: grouping2
195
+ };
196
+ }
197
+ const grouping = { ...before, keys };
198
+ const removed = new Set(before.keys.filter((key) => !keys.includes(key)));
199
+ const columns = current.select.length ? current.select : before.keys.map((field) => ({ field }));
200
+ return {
201
+ ...current,
202
+ grouping,
203
+ offset: 0,
204
+ aggregations: [],
205
+ select: [
206
+ ...keys.filter((key) => !before.keys.includes(key)).map((field) => ({ field })),
207
+ ...columns.filter((c) => !removed.has(c.field))
208
+ ],
209
+ where: current.where.filter(
210
+ (term) => !predicatesOf2(term).some((p) => removed.has(p.field))
211
+ ),
212
+ orderBy: current.orderBy.filter((term) => !removed.has(term.field))
213
+ };
214
+ });
215
+ setNotice(
216
+ stage && api.query.aggregations?.length && keys.length ? "Grouping changed. Group metrics cleared; Undo restores them." : ""
217
+ );
218
+ api.selection.clear();
219
+ }
220
+ function applyColumns(summaries, columns) {
221
+ if (!stage) {
222
+ api.setQuery((q) => ({ ...q, select: columns }));
223
+ return;
224
+ }
225
+ model.validate({ ...stage, summaries });
226
+ const changed = new Set(
227
+ stage.summaries.filter((before) => {
228
+ const after = summaries.find((column) => column.id === before.id);
229
+ return !after || before.op !== after.op || before.field !== after.field || before.expression !== after.expression;
230
+ }).map((column) => column.id)
231
+ );
232
+ api.setQuery(
233
+ (q) => q.grouping ? {
234
+ ...q,
235
+ grouping: { ...q.grouping, summaries },
236
+ select: columns,
237
+ ...changed.size ? {
238
+ offset: 0,
239
+ aggregations: [],
240
+ where: q.where.filter(
241
+ (term) => !predicatesOf2(term).some((p) => changed.has(p.field))
242
+ ),
243
+ orderBy: q.orderBy.filter((term) => !changed.has(term.field))
244
+ } : {}
245
+ } : q
246
+ );
247
+ setNotice(
248
+ changed.size && (api.query.where.some(
249
+ (term) => predicatesOf2(term).some((p) => changed.has(p.field))
250
+ ) || api.query.orderBy.some((term) => changed.has(term.field)) || api.query.aggregations?.length) ? "Columns updated. Affected filters, sorts, and metrics cleared; Undo restores them." : ""
251
+ );
252
+ }
253
+ function addSummary(op, field) {
254
+ if (!stage || op === "row" && !stage.representative) return;
255
+ const existing = stage.summaries.find(
256
+ (column2) => column2.op === op && column2.field === field
257
+ );
258
+ if (!existing && stage.summaries.length >= MAX_SUMMARY_COLUMNS) {
259
+ setNotice(
260
+ `A grouped view supports up to ${MAX_SUMMARY_COLUMNS} aggregate columns.`
261
+ );
262
+ return;
263
+ }
264
+ const column = existing ?? {
265
+ id: `summary_${crypto.randomUUID().replace(/-/g, "")}`,
266
+ op,
267
+ ...field ? { field } : {}
268
+ };
269
+ applyColumns(
270
+ existing ? stage.summaries : [...stage.summaries, column],
271
+ api.select.visible.some((c) => c.field === column.id) ? api.select.visible : [...api.select.visible, { field: column.id }]
272
+ );
273
+ }
274
+ function changeSummary(id, op) {
275
+ if (!stage) return;
276
+ applyColumns(
277
+ stage.summaries.map(
278
+ (column) => column.id === id ? { ...column, op } : column
279
+ ),
280
+ api.select.visible
281
+ );
282
+ }
283
+ function representativeImpact() {
284
+ if (!stage) return "";
285
+ const before = api.query;
286
+ const after = model.reconcileRepresentative(
287
+ before,
288
+ stage.summaries,
289
+ outputSchema,
290
+ false
291
+ );
292
+ return [
293
+ before.where.length !== after.where.length ? "row-value filters" : "",
294
+ before.orderBy.length !== after.orderBy.length ? "row-value sorts" : "",
295
+ (before.aggregations?.length ?? 0) !== (after.aggregations?.length ?? 0) ? "dependent metrics" : ""
296
+ ].filter(Boolean).join(", ");
297
+ }
298
+ function changeRepresentative(representative) {
299
+ if (!stage || JSON.stringify(stage.representative) === JSON.stringify(representative))
300
+ return;
301
+ const summaries = representative ? stage.summaries : stage.summaries.filter((column) => column.op !== "row");
302
+ model.validate({ ...stage, summaries, representative });
303
+ const impact = representativeImpact();
304
+ api.setQuery(
305
+ (q) => q.grouping ? normalizeQueryState({
306
+ ...model.reconcileRepresentative(
307
+ q,
308
+ stage.summaries,
309
+ outputSchema,
310
+ !representative
311
+ ),
312
+ grouping: { ...q.grouping, summaries, representative }
313
+ }) : q
314
+ );
315
+ setNotice(
316
+ impact ? `Representative row changed. Cleared ${impact}; Undo restores them.` : ""
317
+ );
318
+ }
319
+ return {
320
+ ...execution,
321
+ supported,
322
+ model,
323
+ api,
324
+ source,
325
+ grouped: api,
326
+ schema: api.schema,
327
+ view,
328
+ inputs,
329
+ notice,
330
+ setNotice,
331
+ changeKeys,
332
+ addSummary,
333
+ changeSummary,
334
+ applyColumns,
335
+ changeRepresentative,
336
+ representativeImpact
337
+ };
338
+ }
339
+
340
+ // src/useQueryTable.ts
341
+ import {
342
+ createGroupingModel,
343
+ prepareGrouping,
344
+ withoutGrouping as withoutGrouping2,
345
+ sourceExecutionSchema,
346
+ toQueryRequestV3,
347
+ validateQueryResultV3
348
+ } from "@pythia-software/query-table-core";
349
+ import { ITEM_DETAILS_COLUMN, itemPageFields } from "@pythia-software/query-table-core";
350
+
351
+ // src/useItemDetails.ts
352
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
353
+
354
+ // src/usePropertyView.ts
355
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
356
+ import {
357
+ createPropertyView,
358
+ memoryPropertyViewStorage,
359
+ normalizePropertyView
360
+ } from "@pythia-software/query-table-core";
361
+ var asError = (error) => error instanceof Error ? error : new Error(String(error));
362
+ function usePropertyView(options) {
363
+ const [memory] = useState2(memoryPropertyViewStorage);
364
+ const storage = options.storage ?? memory;
365
+ const key = options.key ?? options.schema.name;
366
+ const schemaDefault = useMemo2(() => createPropertyView(options.schema), [options.schema]);
367
+ const fallback = useMemo2(
368
+ () => options.initialView ? normalizePropertyView(options.initialView) : schemaDefault,
369
+ [options.initialView, schemaDefault]
370
+ );
371
+ const defaults = useRef2(schemaDefault);
372
+ defaults.current = schemaDefault;
373
+ const initial = useRef2(options.initialView);
374
+ initial.current = options.initialView;
375
+ const [state, setState] = useState2({
376
+ key,
377
+ storage,
378
+ history: [fallback],
379
+ index: 0,
380
+ loading: true,
381
+ error: null,
382
+ saved: [],
383
+ defaultId: null
384
+ });
385
+ const generation = useRef2(0);
386
+ const edits = useRef2(0);
387
+ const saveQueue = useRef2(Promise.resolve());
388
+ const current = state.key === key && state.storage === storage;
389
+ const view = current ? state.history[state.index] : fallback;
390
+ useEffect2(() => {
391
+ const token = ++generation.current;
392
+ const editToken = edits.current;
393
+ setState({
394
+ key,
395
+ storage,
396
+ history: [defaults.current],
397
+ index: 0,
398
+ loading: true,
399
+ error: null,
400
+ saved: [],
401
+ defaultId: null
402
+ });
403
+ void Promise.resolve().then(
404
+ () => Promise.all([
405
+ storage.loadLast(key),
406
+ storage.listSaved(key),
407
+ storage.loadDefaultSaved(key)
408
+ ])
409
+ ).then(([last, saved, defaultView]) => {
410
+ if (generation.current !== token) return;
411
+ const restored = normalizePropertyView(
412
+ initial.current ?? last ?? defaultView?.view ?? defaults.current
413
+ );
414
+ const items = saved.map((item) => ({
415
+ ...item,
416
+ view: normalizePropertyView(item.view)
417
+ }));
418
+ setState((previous) => ({
419
+ ...previous,
420
+ ...edits.current === editToken ? { history: [restored], index: 0 } : {},
421
+ saved: items,
422
+ defaultId: defaultView?.id ?? null,
423
+ loading: false
424
+ }));
425
+ }).catch((error) => {
426
+ if (generation.current === token)
427
+ setState((previous) => ({
428
+ ...previous,
429
+ loading: false,
430
+ error: asError(error)
431
+ }));
432
+ });
433
+ return () => {
434
+ generation.current++;
435
+ };
436
+ }, [key, storage]);
437
+ useEffect2(() => {
438
+ if (!current || state.loading || state.error) return;
439
+ const token = generation.current;
440
+ const snapshot = normalizePropertyView(view);
441
+ saveQueue.current = saveQueue.current.then(() => storage.saveLast(key, snapshot)).catch((error) => {
442
+ if (generation.current === token)
443
+ setState((previous) => ({ ...previous, error: asError(error) }));
444
+ });
445
+ }, [view, current, state.loading, state.error, key, storage]);
446
+ const setView = useCallback2(
447
+ (next) => {
448
+ const normalized = normalizePropertyView(next);
449
+ edits.current++;
450
+ setState((previous) => {
451
+ const history = previous.key === key && previous.storage === storage ? previous.history.slice(0, previous.index + 1) : [defaults.current];
452
+ if (JSON.stringify(history[history.length - 1]) === JSON.stringify(normalized))
453
+ return { ...previous, error: null };
454
+ const nextHistory = [...history, normalized].slice(-50);
455
+ return {
456
+ ...previous,
457
+ key,
458
+ storage,
459
+ history: nextHistory,
460
+ index: nextHistory.length - 1,
461
+ error: null
462
+ };
463
+ });
464
+ },
465
+ [key, storage]
466
+ );
467
+ const refresh = useCallback2(
468
+ async (token) => {
469
+ const [saved, defaultView] = await Promise.all([
470
+ storage.listSaved(key),
471
+ storage.loadDefaultSaved(key)
472
+ ]);
473
+ if (generation.current === token)
474
+ setState((previous) => ({
475
+ ...previous,
476
+ saved: saved.map((item) => ({
477
+ ...item,
478
+ view: normalizePropertyView(item.view)
479
+ })),
480
+ defaultId: defaultView?.id ?? null,
481
+ error: null
482
+ }));
483
+ },
484
+ [key, storage]
485
+ );
486
+ return {
487
+ view,
488
+ loading: !current || state.loading,
489
+ error: current ? state.error : null,
490
+ saved: current ? state.saved : [],
491
+ defaultId: current ? state.defaultId : null,
492
+ setView,
493
+ reset: () => setView(defaults.current),
494
+ canUndo: current && state.index > 0,
495
+ canRedo: current && state.index < state.history.length - 1,
496
+ undo() {
497
+ edits.current++;
498
+ setState((previous) => ({
499
+ ...previous,
500
+ index: Math.max(0, previous.index - 1),
501
+ error: null
502
+ }));
503
+ },
504
+ redo() {
505
+ edits.current++;
506
+ setState((previous) => ({
507
+ ...previous,
508
+ index: Math.min(previous.history.length - 1, previous.index + 1),
509
+ error: null
510
+ }));
511
+ },
512
+ load(id) {
513
+ const found = state.saved.find((item) => item.id === id);
514
+ if (found && current) setView(found.view);
515
+ },
516
+ async save(name) {
517
+ const token = generation.current;
518
+ const saved = await storage.saveNamed(key, name, view, Date.now());
519
+ await refresh(token);
520
+ return saved;
521
+ },
522
+ async remove(id) {
523
+ const token = generation.current;
524
+ await storage.deleteSaved(key, id);
525
+ await refresh(token);
526
+ },
527
+ async setDefault(id) {
528
+ const token = generation.current;
529
+ await storage.setDefaultSaved(key, id);
530
+ await refresh(token);
531
+ }
532
+ };
533
+ }
534
+
535
+ // src/useItemDetails.ts
536
+ var ALL_MODES = ["hover", "modal", "expand", "page"];
537
+ function initialDetailModes(config) {
538
+ const available = ALL_MODES.filter((mode) => !config?.modes || config.modes.includes(mode));
539
+ return config?.defaultModes ? available.filter((mode) => config.defaultModes.includes(mode)) : available.includes("modal") ? ["modal"] : available.slice(0, 1);
540
+ }
541
+ function useDetailProperties(mode, schema, config, query, setQuery, enabled) {
542
+ const snapshot = query.itemDetails?.views?.[mode] ?? query.itemDetails?.view;
543
+ const seed = useRef3(snapshot);
544
+ const properties = usePropertyView({
545
+ schema,
546
+ ...config?.storage ? { storage: config.storage } : {},
547
+ key: `${config?.objectType ?? schema.name}:${mode}`,
548
+ ...seed.current ? { initialView: seed.current } : {}
549
+ });
550
+ const queryViewKey = JSON.stringify(snapshot);
551
+ const localViewKey = JSON.stringify(properties.view);
552
+ const previousView = useRef3({ query: queryViewKey, local: localViewKey });
553
+ const baselineView = useRef3(properties.view);
554
+ const baselineReady = useRef3(false);
555
+ const pendingView = useRef3(null);
556
+ useEffect3(() => {
557
+ if (!enabled) return;
558
+ if (!properties.loading && !baselineReady.current) {
559
+ baselineView.current = properties.view;
560
+ baselineReady.current = true;
561
+ }
562
+ const previous = previousView.current;
563
+ previousView.current = { query: queryViewKey, local: localViewKey };
564
+ if (queryViewKey !== previous.query) {
565
+ const next = snapshot ?? baselineView.current;
566
+ const target = JSON.stringify(next);
567
+ pendingView.current = target !== localViewKey ? { query: queryViewKey, local: target } : null;
568
+ if (target !== localViewKey) properties.setView(next);
569
+ } else if (pendingView.current && pendingView.current.query === queryViewKey) {
570
+ if (pendingView.current.local === localViewKey) pendingView.current = null;
571
+ } else if (!properties.loading && localViewKey !== queryViewKey && (queryViewKey !== void 0 || localViewKey !== previous.local)) {
572
+ setQuery((current) => ({ ...current, itemDetails: {
573
+ modes: current.itemDetails?.modes ?? initialDetailModes(config),
574
+ ...current.itemDetails,
575
+ views: { ...current.itemDetails?.views, [mode]: properties.view }
576
+ } }));
577
+ }
578
+ }, [enabled, queryViewKey, localViewKey, properties.loading, properties.setView, setQuery]);
579
+ return properties;
580
+ }
581
+ function useItemDetails({
582
+ schema,
583
+ query,
584
+ setQuery,
585
+ options,
586
+ rows,
587
+ clientRows,
588
+ rowId,
589
+ transport,
590
+ refreshKey,
591
+ links,
592
+ queryReady = true,
593
+ queryParameter: queryParameter2
594
+ }) {
595
+ const enabled = Boolean(options);
596
+ const config = typeof options === "object" ? options : void 0;
597
+ const detailSchema = config?.schema ?? schema;
598
+ const availableModes = ALL_MODES.filter((mode) => !config?.modes || config.modes.includes(mode));
599
+ const modes = availableModes.filter((mode) => (query.itemDetails?.modes ?? initialDetailModes(config)).includes(mode));
600
+ const page = query.itemDetails?.page ?? config?.page ?? { urlTemplate: "", target: "_blank" };
601
+ const requested = query.itemDetails?.active;
602
+ const sharedModal = requested?.mode === "modal" && query.itemDetails?.modes.includes("modal") && (availableModes.includes("hover") || availableModes.includes("page") && !page.urlTemplate.trim());
603
+ const active = requested && (modes.includes(requested.mode) || sharedModal) ? requested : null;
604
+ const syncProperties = enabled && queryReady;
605
+ const modalProperties = useDetailProperties("modal", detailSchema, config, query, setQuery, syncProperties);
606
+ const hoverProperties = useDetailProperties("hover", detailSchema, config, query, setQuery, syncProperties);
607
+ const expandProperties = useDetailProperties("expand", detailSchema, config, query, setQuery, syncProperties);
608
+ const propertiesByMode = { modal: modalProperties, hover: hoverProperties, expand: expandProperties };
609
+ const views = {
610
+ modal: modalProperties.view,
611
+ hover: hoverProperties.view,
612
+ expand: expandProperties.view
613
+ };
614
+ const customLoad = config?.loadRow;
615
+ const customLoadRef = useRef3(customLoad);
616
+ customLoadRef.current = customLoad;
617
+ const hasCustomLoad = Boolean(customLoad);
618
+ const sourceType = config?.objectType ?? detailSchema.name;
619
+ const fallbackRows = !customLoad && !clientRows && !transport?.fetchRow ? rows : void 0;
620
+ const loadRow = useCallback3(async (id, signal) => {
621
+ if (customLoadRef.current) return customLoadRef.current(id, signal);
622
+ if (clientRows) {
623
+ const row2 = clientRows.find((row3) => rowId(row3) === id);
624
+ return row2 ? { row: row2 } : null;
625
+ }
626
+ if (transport?.fetchRow) {
627
+ const row2 = await transport.fetchRow(id, signal);
628
+ return row2 ? { row: row2 } : null;
629
+ }
630
+ const row = rows.find((row2) => rowId(row2) === id);
631
+ return row ? { row, loadedFields: Object.keys(row) } : null;
632
+ }, [hasCustomLoad, sourceType, clientRows, transport?.fetchRow, fallbackRows, rowId]);
633
+ const [retryKey, setRetryKey] = useState3(0);
634
+ const [result, setResult] = useState3({ source: void 0, epoch: "", id: void 0, record: null, loading: false, error: null });
635
+ const activeId = enabled ? active?.id : void 0;
636
+ const epoch = `${refreshKey}:${retryKey}`;
637
+ const currentResult = result.id === activeId && result.source === loadRow && result.epoch === epoch;
638
+ useEffect3(() => {
639
+ const controller = new AbortController();
640
+ if (activeId === void 0) {
641
+ setResult((previous) => previous.id === void 0 && !previous.loading && !previous.error && !previous.record ? previous : { source: void 0, epoch: "", id: void 0, record: null, loading: false, error: null });
642
+ return;
643
+ }
644
+ setResult({ source: loadRow, epoch, id: activeId, record: null, loading: true, error: null });
645
+ void Promise.resolve().then(() => loadRow(activeId, controller.signal)).then((record) => {
646
+ if (!controller.signal.aborted) setResult({ source: loadRow, epoch, id: activeId, record, loading: false, error: null });
647
+ }).catch((error) => {
648
+ if (!controller.signal.aborted) setResult({
649
+ source: loadRow,
650
+ epoch,
651
+ id: activeId,
652
+ record: null,
653
+ loading: false,
654
+ error: error instanceof Error ? error : new Error(String(error))
655
+ });
656
+ });
657
+ return () => controller.abort();
658
+ }, [activeId, loadRow, refreshKey, retryKey]);
659
+ if (!enabled) return;
660
+ return {
661
+ ...links ? { links } : {},
662
+ schema: detailSchema,
663
+ queryParameter: queryParameter2 ?? config?.queryParameter ?? "q",
664
+ properties: modalProperties,
665
+ propertiesByMode,
666
+ availableModes,
667
+ modes,
668
+ active,
669
+ page,
670
+ pageSchema: schema,
671
+ record: currentResult ? result.record : null,
672
+ loading: activeId !== void 0 && (!currentResult || result.loading),
673
+ error: currentResult ? result.error : null,
674
+ ...config?.relationships ? { relationships: config.relationships } : {},
675
+ loadRow,
676
+ retry: () => setRetryKey((value) => value + 1),
677
+ setModes(next) {
678
+ setQuery((current) => {
679
+ const allowed = availableModes.filter((mode) => next.includes(mode));
680
+ const state = { ...current.itemDetails, modes: allowed, views };
681
+ if (state.active && !allowed.includes(state.active.mode)) delete state.active;
682
+ return { ...current, itemDetails: state };
683
+ });
684
+ },
685
+ open(id, mode) {
686
+ if (!modes.includes(mode)) return;
687
+ setQuery((current) => ({ ...current, itemDetails: {
688
+ ...current.itemDetails,
689
+ modes: current.itemDetails?.modes ?? modes,
690
+ views,
691
+ active: { id, mode }
692
+ } }));
693
+ },
694
+ close() {
695
+ setQuery((current) => {
696
+ if (!current.itemDetails?.active) return current;
697
+ const { active: _active, ...state } = current.itemDetails;
698
+ return { ...current, itemDetails: state };
699
+ });
700
+ }
701
+ };
702
+ }
703
+
1
704
  // src/computedExecution.ts
2
705
  import {
3
706
  isComputedField,
@@ -12,8 +715,13 @@ function validateComputedResponse(request, result, schema) {
12
715
  throw Error(
13
716
  "Server computed execution identity changed. Refresh the handshake."
14
717
  );
718
+ const executed = /* @__PURE__ */ new Set([
719
+ ...request.select.filter(isComputedField),
720
+ ...request.orderBy.map((term) => term.field).filter(isComputedField)
721
+ ]);
15
722
  for (const [name, revision] of Object.entries(request.expectedRevisions)) {
16
723
  const id = name.replace(/^@computed\//, "");
724
+ if (!executed.has(`@computed/${id}`)) continue;
17
725
  if (executionRevision(execution, id) !== revision)
18
726
  throw Error(
19
727
  "Server computed definition revision changed. Refresh the handshake."
@@ -43,7 +751,7 @@ function validateComputedResponse(request, result, schema) {
43
751
  }
44
752
 
45
753
  // src/useComputedColumns.ts
46
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
754
+ import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
47
755
  import {
48
756
  applyQuery,
49
757
  compileFormula,
@@ -148,18 +856,18 @@ function evaluateFormulaRows(ast, inputs, signal, factory = createFormulaWorker)
148
856
  // src/useComputedColumns.ts
149
857
  function useComputedColumns(options) {
150
858
  const { schema, query, rows, transport, clientRows, workerFactory } = options;
151
- const store = useMemo(
859
+ const store = useMemo3(
152
860
  () => options.store ?? memoryComputedColumnStore(),
153
861
  [options.store]
154
862
  );
155
- const [catalogue, setCatalogue] = useState(null);
156
- const definitions = useMemo(
863
+ const [catalogue, setCatalogue] = useState4(null);
864
+ const definitions = useMemo3(
157
865
  () => catalogue?.dataset === schema.name && catalogue.store === store ? catalogue.items : [],
158
866
  [catalogue, schema.name, store]
159
867
  );
160
- const [loading, setLoading] = useState(false), [error, setError] = useState(null);
161
- const catalogueRequest = useRef(null);
162
- const reload = useCallback(async () => {
868
+ const [loading, setLoading] = useState4(false), [error, setError] = useState4(null);
869
+ const catalogueRequest = useRef4(null);
870
+ const reload = useCallback4(async () => {
163
871
  catalogueRequest.current?.abort();
164
872
  const ac = new AbortController();
165
873
  catalogueRequest.current = ac;
@@ -181,30 +889,30 @@ function useComputedColumns(options) {
181
889
  if (!ac.signal.aborted) setLoading(false);
182
890
  }
183
891
  }, [store, schema.name]);
184
- useEffect(() => {
892
+ useEffect4(() => {
185
893
  void reload();
186
894
  return () => catalogueRequest.current?.abort();
187
895
  }, [reload]);
188
- useEffect(
896
+ useEffect4(
189
897
  () => store.subscribe?.(schema.name, () => {
190
898
  void reload();
191
899
  }),
192
900
  [store, schema.name, reload]
193
901
  );
194
- useEffect(() => {
902
+ useEffect4(() => {
195
903
  const focus = () => {
196
904
  void reload();
197
905
  };
198
906
  window.addEventListener("focus", focus);
199
907
  return () => window.removeEventListener("focus", focus);
200
908
  }, [reload]);
201
- const inputFields = useMemo(
909
+ const inputFields = useMemo3(
202
910
  () => schema.fields.filter(isSelectable).filter(
203
911
  (f) => f.source.kind === "backend" || f.source.accessor && f.source.dependencies
204
912
  ),
205
913
  [schema]
206
914
  );
207
- const compile = useCallback(
915
+ const compile = useCallback4(
208
916
  (source, editingId) => {
209
917
  const stack = new Set(editingId ? [editingId] : []);
210
918
  let expanded = 0;
@@ -227,7 +935,7 @@ function useComputedColumns(options) {
227
935
  },
228
936
  [inputFields, definitions]
229
937
  );
230
- const plans = useMemo(
938
+ const plans = useMemo3(
231
939
  () => new Map(
232
940
  definitions.map((def) => {
233
941
  try {
@@ -245,7 +953,7 @@ function useComputedColumns(options) {
245
953
  ),
246
954
  [definitions, compile]
247
955
  );
248
- const backendDependencies = useCallback(
956
+ const backendDependencies = useCallback4(
249
957
  (plan) => [
250
958
  ...new Set(
251
959
  plan.dependencies.flatMap((name) => {
@@ -260,7 +968,7 @@ function useComputedColumns(options) {
260
968
  ).sort(),
261
969
  [schema, inputFields]
262
970
  );
263
- const inputsFor = useCallback(
971
+ const inputsFor = useCallback4(
264
972
  (plan, batch) => {
265
973
  let size = 0;
266
974
  return batch.map((row) => {
@@ -282,13 +990,14 @@ function useComputedColumns(options) {
282
990
  );
283
991
  const activeSelect = query.select.length ? query.select : schema.defaultSelect ?? [];
284
992
  const activeIds = activeSelect.filter((c) => isComputedField2(c.field)).map((c) => c.field.slice("@computed/".length)).sort().join(",");
285
- const [evaluated, setEvaluated] = useState(null);
286
- useEffect(() => {
993
+ const [evaluated, setEvaluated] = useState4(null);
994
+ useEffect4(() => {
287
995
  const ac = new AbortController();
288
996
  const values = /* @__PURE__ */ new Map();
289
997
  void (async () => {
290
- if (options.execution && transport?.fetchRowsV2) return;
291
998
  for (const id of activeIds.split(",").filter(Boolean)) {
999
+ if (options.execution?.fields[computedFieldName(id)]?.select && (transport?.fetchRowsV2 || transport?.fetchQueryV3))
1000
+ continue;
292
1001
  const entry = plans.get(id);
293
1002
  if (!entry?.plan) continue;
294
1003
  try {
@@ -324,7 +1033,7 @@ function useComputedColumns(options) {
324
1033
  options.execution,
325
1034
  transport
326
1035
  ]);
327
- const displaySchema = useMemo(() => {
1036
+ const displaySchema = useMemo3(() => {
328
1037
  const indices = new Map(rows.map((row, i) => [row, i]));
329
1038
  const fields = definitions.map((def) => {
330
1039
  const entry = plans.get(def.id);
@@ -339,7 +1048,7 @@ function useComputedColumns(options) {
339
1048
  dependencies: entry?.plan ? backendDependencies(entry.plan) : [],
340
1049
  accessor: (row) => {
341
1050
  if (entry?.error) return { computedError: entry.error };
342
- if (options.execution && transport?.fetchRowsV2) {
1051
+ if (options.execution?.fields[computedFieldName(def.id)]?.select && (transport?.fetchRowsV2 || transport?.fetchQueryV3)) {
343
1052
  if (options.serverExecution?.profile !== options.execution.profile || options.serverExecution?.planToken !== options.execution.planToken || options.serverExecution?.snapshot !== options.execution.snapshot || executionRevision(options.execution, def.id) !== def.revision || executionRevision(options.serverExecution, def.id) !== def.revision)
344
1053
  return {
345
1054
  computedError: "Definition changed or values are pending. Refresh the server handshake."
@@ -355,13 +1064,13 @@ function useComputedColumns(options) {
355
1064
  return result?.error ? { computedError: result.error } : result ? result.value : { computedError: "Calculating\u2026" };
356
1065
  }
357
1066
  },
358
- filter: { enabled: false },
1067
+ filter: { enabled: options.execution?.fields[computedFieldName(def.id)]?.filter === true && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan) },
359
1068
  sort: {
360
- enabled: !!transport?.fetchRowsV2 && !!options.execution?.fields[computedFieldName(def.id)]?.sort && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
1069
+ enabled: !!(transport?.fetchRowsV2 || transport?.fetchQueryV3) && !!options.execution?.fields[computedFieldName(def.id)]?.sort && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
361
1070
  },
362
1071
  aggregate: {
363
1072
  measure: !!entry?.plan,
364
- groupable: !!transport?.fetchMetrics && transport.metricCapabilities?.version === 2 && !!options.execution?.fields[computedFieldName(def.id)]?.group && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
1073
+ groupable: (!!transport?.fetchMetrics && transport.metricCapabilities?.version === 2 || !!transport?.fetchQueryV3) && !!options.execution?.fields[computedFieldName(def.id)]?.group && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
365
1074
  }
366
1075
  };
367
1076
  });
@@ -398,7 +1107,7 @@ function useComputedColumns(options) {
398
1107
  options.serverComputed,
399
1108
  options.serverExecution
400
1109
  ]);
401
- const save = useCallback(
1110
+ const save = useCallback4(
402
1111
  async (column, expectedRevision) => {
403
1112
  compile(column.expression.source, column.id);
404
1113
  const saved = validateComputedColumn(
@@ -416,7 +1125,7 @@ function useComputedColumns(options) {
416
1125
  },
417
1126
  [compile, store, schema.name]
418
1127
  );
419
- const preview = useCallback(
1128
+ const preview = useCallback4(
420
1129
  async (source, count, signal, editingId, includeInputs = true) => {
421
1130
  if (!Number.isInteger(count) || count < 1 || count > 1e4)
422
1131
  throw new Error("Choose between 1 and 10,000 rows.");
@@ -497,34 +1206,31 @@ function useComputedColumns(options) {
497
1206
  }
498
1207
 
499
1208
  // src/useQueryTable.ts
500
- import { useCallback as useCallback8, useEffect as useEffect5, useMemo as useMemo7, useRef as useRef5, useState as useState7 } from "react";
1209
+ import { useCallback as useCallback12, useEffect as useEffect10, useMemo as useMemo9, useRef as useRef9, useState as useState11 } from "react";
501
1210
  import {
502
1211
  EMPTY_QUERY,
503
1212
  applyQuery as applyQuery2,
504
- decodeQuery,
505
- encodeQuery,
506
1213
  memoryStorageAdapter,
507
- normalizeQueryState as normalizeQueryState2,
1214
+ normalizeQueryState as normalizeQueryState3,
508
1215
  readFieldValue as readFieldValue4,
509
1216
  selectedFields as selectedFields2,
510
1217
  toServerQuery as toServerQuery2,
511
- queriesEqual,
512
- isOrGroup,
513
- predicatesOf,
514
- negateClause,
515
- opsForField,
1218
+ queriesEqual as queriesEqual2,
1219
+ isOrGroup as isOrGroup2,
516
1220
  metricPlans as metricPlans3,
517
1221
  validateMetric as validateMetric2,
518
1222
  metricDependencies,
519
1223
  isComputedField as isComputedField4,
1224
+ computedFieldName as computedFieldName2,
520
1225
  toServerQueryV2
521
1226
  } from "@pythia-software/query-table-core";
522
1227
 
523
1228
  // src/useRequestActivity.ts
524
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
1229
+ import { useCallback as useCallback5, useEffect as useEffect5, useMemo as useMemo4, useRef as useRef5, useState as useState5 } from "react";
525
1230
  var methods = /* @__PURE__ */ new Set([
526
1231
  "fetchRows",
527
1232
  "fetchRowsV2",
1233
+ "fetchQueryV3",
528
1234
  "fetchMetrics",
529
1235
  "fetchAggregations",
530
1236
  "fetchRow",
@@ -584,29 +1290,29 @@ function observeTransport(transport, publish, nextId) {
584
1290
  });
585
1291
  }
586
1292
  function useRequestActivity(source) {
587
- const [entries, setEntries] = useState2([]);
588
- const sequence = useRef2(0), mounted = useRef2(true);
589
- useEffect2(() => {
1293
+ const [entries, setEntries] = useState5([]);
1294
+ const sequence = useRef5(0), mounted = useRef5(true);
1295
+ useEffect5(() => {
590
1296
  mounted.current = true;
591
1297
  return () => {
592
1298
  mounted.current = false;
593
1299
  };
594
1300
  }, []);
595
- const publish = useCallback2((entry) => {
1301
+ const publish = useCallback5((entry) => {
596
1302
  if (mounted.current)
597
1303
  setEntries(
598
1304
  (previous) => [entry, ...previous.filter((e) => e.id !== entry.id)].sort((a, b) => b.id - a.id).slice(0, 100)
599
1305
  );
600
1306
  }, []);
601
- const transport = useMemo2(
1307
+ const transport = useMemo4(
602
1308
  () => source ? observeTransport(source, publish, () => ++sequence.current) : void 0,
603
1309
  [source, publish]
604
1310
  );
605
- const clear = useCallback2(
1311
+ const clear = useCallback5(
606
1312
  () => setEntries((previous) => previous.filter((e) => e.status === "pending")),
607
1313
  []
608
1314
  );
609
- const activity = useMemo2(
1315
+ const activity = useMemo4(
610
1316
  () => ({ entries, clear }),
611
1317
  [entries, clear]
612
1318
  );
@@ -614,7 +1320,7 @@ function useRequestActivity(source) {
614
1320
  }
615
1321
 
616
1322
  // src/useAggregations.ts
617
- import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
1323
+ import { useCallback as useCallback6, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef6, useState as useState6 } from "react";
618
1324
  import {
619
1325
  metricComputationKey,
620
1326
  metricPlans as metricPlans2,
@@ -624,7 +1330,7 @@ import {
624
1330
  // src/metricExecution.ts
625
1331
  import {
626
1332
  applyAggregations,
627
- normalizeQueryState,
1333
+ normalizeQueryState as normalizeQueryState2,
628
1334
  evaluateMetrics,
629
1335
  metricPlans,
630
1336
  hasRegex as hasRegex2,
@@ -642,10 +1348,11 @@ var abort = (signal) => {
642
1348
  async function localMetrics(rows, query, context, signal) {
643
1349
  const originalClauses = query.aggregations ?? [];
644
1350
  const advanced = (query.aggregations ?? []).filter(
645
- (c) => isComputedField3(c.field ?? "") || c.groupBy.some(isComputedField3) || c.diagnostics?.length || c.expression !== void 0 || c.expressionY !== void 0 || c.scope !== void 0 || c.distribution || c.sort || c.groupLimit !== void 0
1351
+ (c) => context.strictLocalMetrics || c.dateGrouping || isComputedField3(c.field ?? "") || c.groupBy.some(isComputedField3) || c.diagnostics?.length || c.expression !== void 0 || c.expressionY !== void 0 || c.scope !== void 0 || c.distribution || c.sort || c.groupLimit !== void 0
646
1352
  );
647
- if (advanced.length && (originalClauses.length > 20 || normalizeQueryState(query).aggregations?.length !== originalClauses.length))
1353
+ if (advanced.length && (originalClauses.length > 20 || normalizeQueryState2(query).aggregations?.length !== originalClauses.length))
648
1354
  return evaluateMetrics(rows, query, context.schema, {
1355
+ ...context.now !== void 0 ? { now: context.now } : {},
649
1356
  ...signal ? { signal } : {},
650
1357
  ...context.resolveComputed ? { resolveComputed: context.resolveComputed } : {}
651
1358
  });
@@ -657,12 +1364,14 @@ async function localMetrics(rows, query, context, signal) {
657
1364
  { ...query, aggregations: legacy },
658
1365
  context.schema,
659
1366
  {
1367
+ ...context.now !== void 0 ? { now: context.now } : {},
660
1368
  ...signal ? { signal } : {}
661
1369
  }
662
1370
  );
663
1371
  if (!advanced.length) return compatible;
664
1372
  query = { ...query, aggregations: advanced };
665
1373
  const options = {
1374
+ ...context.now !== void 0 ? { now: context.now } : {},
666
1375
  ...context.resolveComputed ? { resolveComputed: context.resolveComputed } : {},
667
1376
  ...signal ? { signal } : {}
668
1377
  };
@@ -749,15 +1458,16 @@ async function executeMetrics(query, context, signal) {
749
1458
  abort(signal);
750
1459
  const clauses = query.aggregations ?? [];
751
1460
  if (!clauses.length) return { metrics: [] };
1461
+ if (context.executeV3) return context.executeV3(query, signal);
752
1462
  const supportsV2 = !!context.transport?.fetchMetrics && context.transport.metricCapabilities?.version === 2;
753
1463
  if (context.clientRows && !supportsV2 && !context.transport?.fetchAggregations)
754
1464
  return localMetrics(context.clientRows, query, context, signal);
755
1465
  const needsWorker = (c) => metricPlans(c, context.schema, context.resolveComputed).some(hasRegex2);
756
1466
  const shown = clauses.filter(
757
- (c) => c.scope === "shownRows" && (!supportsV2 || needsWorker(c))
1467
+ (c) => c.scope === "shownRows" && (!supportsV2 || needsWorker(c) || !!c.dateGrouping)
758
1468
  );
759
1469
  const matching = clauses.filter(
760
- (c) => c.scope !== "shownRows" || supportsV2 && !needsWorker(c)
1470
+ (c) => c.scope !== "shownRows" || supportsV2 && !needsWorker(c) && !c.dateGrouping
761
1471
  );
762
1472
  const results = [];
763
1473
  if (shown.length) {
@@ -790,7 +1500,6 @@ async function executeMetrics(query, context, signal) {
790
1500
  }
791
1501
  const pageQuery = {
792
1502
  ...query,
793
- where: [],
794
1503
  orderBy: [],
795
1504
  limit: Math.max(1, context.shownRows.length),
796
1505
  offset: 0,
@@ -801,6 +1510,10 @@ async function executeMetrics(query, context, signal) {
801
1510
  );
802
1511
  }
803
1512
  if (matching.length) {
1513
+ if (matching.some((c) => c.dateGrouping))
1514
+ throw Error(
1515
+ "This server does not support calendar grouping. Enable the v3 query executor."
1516
+ );
804
1517
  const transport = context.transport;
805
1518
  const scoped = { ...query, aggregations: matching };
806
1519
  if (transport?.fetchMetrics && transport.metricCapabilities?.version === 2) {
@@ -859,7 +1572,7 @@ async function executeMetrics(query, context, signal) {
859
1572
  );
860
1573
  } else
861
1574
  throw Error(
862
- "All-matching metrics require a complete client dataset or a metric-capable server transport."
1575
+ "Remote all-matching metrics require a metric-capable server transport."
863
1576
  );
864
1577
  }
865
1578
  abort(signal);
@@ -887,17 +1600,18 @@ function loadedPath(row, path) {
887
1600
  }
888
1601
 
889
1602
  // src/useAggregations.ts
890
- function useAggregations(query, schema, transport, clientRows, debounceMs, nonce, validateQuery, options = {}) {
891
- const [results, setResults] = useState3(null);
892
- const [loading, setLoading] = useState3(false);
893
- const [error, setError] = useState3(null);
894
- const latest = useRef3({
1603
+ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce, validateQuery, options = {}, enabled = true) {
1604
+ const [results, setResults] = useState6(null);
1605
+ const [loading, setLoading] = useState6(false);
1606
+ const [error, setError] = useState6(null);
1607
+ const latest = useRef6({
895
1608
  query,
896
1609
  schema,
897
1610
  transport,
898
1611
  clientRows,
899
1612
  options,
900
- validateQuery
1613
+ validateQuery,
1614
+ enabled
901
1615
  });
902
1616
  latest.current = {
903
1617
  query,
@@ -905,11 +1619,13 @@ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce
905
1619
  transport,
906
1620
  clientRows,
907
1621
  options,
908
- validateQuery
1622
+ validateQuery,
1623
+ enabled
909
1624
  };
910
- const preview = useCallback3(
1625
+ const preview = useCallback6(
911
1626
  async (clauses, signal) => {
912
1627
  const current = latest.current;
1628
+ if (!current.enabled) throw Error("Resolve the query link before previewing metrics.");
913
1629
  const draft = { ...current.query, aggregations: clauses };
914
1630
  current.validateQuery?.(draft);
915
1631
  return executeMetrics(
@@ -926,7 +1642,7 @@ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce
926
1642
  []
927
1643
  );
928
1644
  const shown = (query.aggregations ?? []).some((c) => c.scope === "shownRows");
929
- const localPage = shown && (!transport?.fetchMetrics || transport.metricCapabilities?.version !== 2 || (query.aggregations ?? []).some((clause) => {
1645
+ const localPage = !options.executeV3 && shown && (!transport?.fetchMetrics || transport.metricCapabilities?.version !== 2 || (query.aggregations ?? []).some((clause) => {
930
1646
  if (clause.scope !== "shownRows") return false;
931
1647
  try {
932
1648
  return metricPlans2(clause, schema, options.resolveComputed).some(
@@ -936,7 +1652,7 @@ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce
936
1652
  return true;
937
1653
  }
938
1654
  }));
939
- const key = useMemo3(
1655
+ const key = useMemo5(
940
1656
  () => JSON.stringify([
941
1657
  query.where,
942
1658
  metricComputationKey(query.aggregations ?? []),
@@ -953,10 +1669,13 @@ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce
953
1669
  );
954
1670
  const dependencyKey = JSON.stringify([
955
1671
  options.revisions ?? {},
956
- options.execution ?? null
1672
+ options.execution ?? null,
1673
+ options.now,
1674
+ options.strictLocalMetrics,
1675
+ options.queryContextKey
957
1676
  ]);
958
- useEffect3(() => {
959
- if (!latest.current.query.aggregations?.length) {
1677
+ useEffect6(() => {
1678
+ if (!enabled || !latest.current.query.aggregations?.length) {
960
1679
  setResults(null);
961
1680
  setLoading(false);
962
1681
  setError(null);
@@ -1006,6 +1725,7 @@ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce
1006
1725
  debounceMs,
1007
1726
  nonce,
1008
1727
  validateQuery,
1728
+ enabled,
1009
1729
  dependencyKey,
1010
1730
  localPage,
1011
1731
  localPage ? options.shownRows : void 0,
@@ -1017,63 +1737,71 @@ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce
1017
1737
  }
1018
1738
 
1019
1739
  // src/useSelection.ts
1020
- import { useCallback as useCallback4, useMemo as useMemo4, useRef as useRef4, useState as useState4 } from "react";
1740
+ import { useCallback as useCallback7, useEffect as useEffect7, useMemo as useMemo6, useState as useState7 } from "react";
1741
+ function sameOrder(left, right) {
1742
+ return left.length === right.length && left.every((id, index) => id === right[index]);
1743
+ }
1021
1744
  function useSelection(displayedIds) {
1022
- const [selected, setSelected] = useState4(() => /* @__PURE__ */ new Set());
1023
- const anchor = useRef4(null);
1024
- const toggle = useCallback4(
1025
- (id, shiftKey = false) => {
1026
- setSelected((prev) => {
1027
- const next = new Set(prev);
1028
- const a = anchor.current;
1029
- if (shiftKey && a != null) {
1030
- const i = displayedIds.indexOf(a);
1745
+ const [{ selected }, setState] = useState7(() => ({
1746
+ selected: /* @__PURE__ */ new Set(),
1747
+ range: null
1748
+ }));
1749
+ useEffect7(() => {
1750
+ setState((prev) => prev.range && !sameOrder(prev.range.displayedIds, displayedIds) ? { selected: prev.selected, range: null } : prev);
1751
+ }, [displayedIds]);
1752
+ const toggle = useCallback7(
1753
+ (id, shiftKey = false, additive = false) => {
1754
+ setState((prev) => {
1755
+ const range = prev.range;
1756
+ if (shiftKey && range && sameOrder(range.displayedIds, displayedIds)) {
1757
+ const i = displayedIds.indexOf(range.anchor);
1031
1758
  const j = displayedIds.indexOf(id);
1032
1759
  if (i !== -1 && j !== -1) {
1033
1760
  const [lo, hi] = i <= j ? [i, j] : [j, i];
1034
- const span = displayedIds.slice(lo, hi + 1);
1035
- const allChecked = span.every((rowId) => prev.has(rowId));
1036
- for (const rowId of span) {
1037
- if (allChecked) next.delete(rowId);
1038
- else next.add(rowId);
1761
+ const activeRange = additive ? { ...range, baseline: prev.selected, selecting: true } : range;
1762
+ const next2 = new Set(activeRange.baseline);
1763
+ for (const rowId of displayedIds.slice(lo, hi + 1)) {
1764
+ if (activeRange.selecting) next2.add(rowId);
1765
+ else next2.delete(rowId);
1039
1766
  }
1040
- anchor.current = id;
1041
- return next;
1767
+ return { selected: next2, range: activeRange };
1042
1768
  }
1043
1769
  }
1044
- if (next.has(id)) next.delete(id);
1045
- else next.add(id);
1046
- anchor.current = id;
1047
- return next;
1770
+ const next = new Set(prev.selected);
1771
+ const selecting = !next.has(id);
1772
+ if (selecting) next.add(id);
1773
+ else next.delete(id);
1774
+ return {
1775
+ selected: next,
1776
+ range: { anchor: id, baseline: prev.selected, selecting, displayedIds: [...displayedIds] }
1777
+ };
1048
1778
  });
1049
1779
  },
1050
1780
  [displayedIds]
1051
1781
  );
1052
- const setPage = useCallback4((ids, select) => {
1053
- setSelected((prev) => {
1054
- const next = new Set(prev);
1782
+ const setPage = useCallback7((ids, select) => {
1783
+ setState((prev) => {
1784
+ const next = new Set(prev.selected);
1055
1785
  for (const id of ids) {
1056
1786
  if (select) next.add(id);
1057
1787
  else next.delete(id);
1058
1788
  }
1059
- return next;
1789
+ return { selected: next, range: null };
1060
1790
  });
1061
1791
  }, []);
1062
- const replace = useCallback4((ids) => {
1792
+ const replace = useCallback7((ids) => {
1063
1793
  const next = new Set(ids);
1064
- anchor.current = null;
1065
- setSelected(next);
1794
+ setState({ selected: next, range: null });
1066
1795
  }, []);
1067
- const retain = useCallback4((ids) => {
1796
+ const retain = useCallback7((ids) => {
1068
1797
  const allowed = new Set(ids);
1069
- anchor.current = null;
1070
- setSelected((prev) => {
1798
+ setState((prev) => {
1071
1799
  const next = /* @__PURE__ */ new Set();
1072
- for (const id of prev) if (allowed.has(id)) next.add(id);
1073
- return next;
1800
+ for (const id of prev.selected) if (allowed.has(id)) next.add(id);
1801
+ return { selected: next, range: null };
1074
1802
  });
1075
1803
  }, []);
1076
- const pageState = useCallback4(
1804
+ const pageState = useCallback7(
1077
1805
  (pageIds) => {
1078
1806
  if (pageIds.length === 0) return "none";
1079
1807
  let n = 0;
@@ -1082,11 +1810,10 @@ function useSelection(displayedIds) {
1082
1810
  },
1083
1811
  [selected]
1084
1812
  );
1085
- const clear = useCallback4(() => {
1086
- anchor.current = null;
1087
- setSelected(/* @__PURE__ */ new Set());
1813
+ const clear = useCallback7(() => {
1814
+ setState({ selected: /* @__PURE__ */ new Set(), range: null });
1088
1815
  }, []);
1089
- return useMemo4(
1816
+ return useMemo6(
1090
1817
  () => ({
1091
1818
  selected,
1092
1819
  isSelected: (id) => selected.has(id),
@@ -1103,27 +1830,29 @@ function useSelection(displayedIds) {
1103
1830
  }
1104
1831
 
1105
1832
  // src/useSelect.ts
1106
- import { useCallback as useCallback5, useMemo as useMemo5 } from "react";
1833
+ import { useCallback as useCallback8, useMemo as useMemo7 } from "react";
1107
1834
  import { selectedFields } from "@pythia-software/query-table-core";
1108
1835
  function useSelect(query, setQuery, schema) {
1109
- const fields = useMemo5(() => selectedFields(schema, query), [schema, query]);
1110
- const visible = useMemo5(() => fields.map((f) => columnFor(query.select, f.name)), [fields, query.select]);
1111
- const hidden = useMemo5(() => {
1836
+ const fields = useMemo7(() => selectedFields(schema, query), [schema, query]);
1837
+ const visible = useMemo7(() => fields.map((f) => columnFor(query.select, f.name)), [fields, query.select]);
1838
+ const hidden = useMemo7(() => {
1112
1839
  const shown = new Set(fields.map((f) => f.name));
1113
1840
  return schema.fields.filter((f) => !shown.has(f.name) && (f.select?.enabled ?? true));
1114
1841
  }, [schema, fields]);
1115
- const writeSelect = useCallback5(
1116
- (mutate) => {
1842
+ const writeSelect = useCallback8(
1843
+ (mutate, clearFitToScreen = false) => {
1117
1844
  setQuery((prev) => {
1118
1845
  const current = prev.select.length ? prev.select : visible;
1119
- return { ...prev, select: mutate([...current]) };
1846
+ const next = { ...prev, select: mutate([...current]) };
1847
+ if (clearFitToScreen) delete next.fitToScreen;
1848
+ return next;
1120
1849
  });
1121
1850
  },
1122
1851
  [setQuery, visible]
1123
1852
  );
1124
- const show = useCallback5((field) => writeSelect((c) => c.some((x) => x.field === field) ? c : [...c, { field }]), [writeSelect]);
1125
- const hide = useCallback5((field) => writeSelect((c) => c.filter((x) => x.field !== field)), [writeSelect]);
1126
- const move = useCallback5(
1853
+ const show = useCallback8((field) => writeSelect((c) => c.some((x) => x.field === field) ? c : [...c, { field }]), [writeSelect]);
1854
+ const hide = useCallback8((field) => writeSelect((c) => c.filter((x) => x.field !== field)), [writeSelect]);
1855
+ const move = useCallback8(
1127
1856
  (field, toIndex) => writeSelect((c) => {
1128
1857
  const from = c.findIndex((x) => x.field === field);
1129
1858
  if (from === -1) return c;
@@ -1133,11 +1862,11 @@ function useSelect(query, setQuery, schema) {
1133
1862
  }),
1134
1863
  [writeSelect]
1135
1864
  );
1136
- const setWidth = useCallback5(
1137
- (field, width) => writeSelect((c) => c.map((x) => x.field === field ? { ...x, width: Math.round(width) } : x)),
1865
+ const setWidth = useCallback8(
1866
+ (field, width) => writeSelect((c) => c.map((x) => x.field === field ? { ...x, width: Math.round(width) } : x), true),
1138
1867
  [writeSelect]
1139
1868
  );
1140
- const reset = useCallback5(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);
1869
+ const reset = useCallback8(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);
1141
1870
  return { visible, fields, hidden, show, hide, move, setWidth, reset };
1142
1871
  }
1143
1872
  function columnFor(select, name) {
@@ -1146,11 +1875,11 @@ function columnFor(select, name) {
1146
1875
  }
1147
1876
 
1148
1877
  // src/useColumnDrag.ts
1149
- import { useCallback as useCallback6, useMemo as useMemo6, useState as useState5 } from "react";
1878
+ import { useCallback as useCallback9, useMemo as useMemo8, useState as useState8 } from "react";
1150
1879
  function useColumnDrag() {
1151
- const [state, setState] = useState5(null);
1152
- const [dropRevision, setDropRevision] = useState5(0);
1153
- const start = useCallback6(
1880
+ const [state, setState] = useState8(null);
1881
+ const [dropRevision, setDropRevision] = useState8(0);
1882
+ const start = useCallback9(
1154
1883
  (field, index, order) => setState({
1155
1884
  source: field,
1156
1885
  overIndex: index,
@@ -1159,7 +1888,7 @@ function useColumnDrag() {
1159
1888
  }),
1160
1889
  []
1161
1890
  );
1162
- const over = useCallback6(
1891
+ const over = useCallback9(
1163
1892
  (index, order) => (
1164
1893
  // Keep the SAME object reference when unchanged so React can bail out of the
1165
1894
  // re-render — onDragOver fires continuously while the cursor moves.
@@ -1177,11 +1906,11 @@ function useColumnDrag() {
1177
1906
  ),
1178
1907
  []
1179
1908
  );
1180
- const end = useCallback6((committed = false) => {
1909
+ const end = useCallback9((committed = false) => {
1181
1910
  if (committed) setDropRevision((revision) => revision + 1);
1182
1911
  setState(null);
1183
1912
  }, []);
1184
- const preview = useCallback6(
1913
+ const preview = useCallback9(
1185
1914
  (order) => {
1186
1915
  if (!state || state.order && !state.moved) return order;
1187
1916
  const without = order.filter((n) => n !== state.source);
@@ -1199,7 +1928,7 @@ function useColumnDrag() {
1199
1928
  },
1200
1929
  [state]
1201
1930
  );
1202
- return useMemo6(
1931
+ return useMemo8(
1203
1932
  () => ({
1204
1933
  source: state?.source ?? null,
1205
1934
  overIndex: state?.overIndex ?? null,
@@ -1215,54 +1944,70 @@ function useColumnDrag() {
1215
1944
  }
1216
1945
 
1217
1946
  // src/useSavedQueries.ts
1218
- import { useCallback as useCallback7, useEffect as useEffect4, useState as useState6 } from "react";
1219
- function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
1220
- const [items, setItems] = useState6([]);
1221
- const [defaultId, setDefaultId] = useState6(null);
1222
- const [loading, setLoading] = useState6(true);
1223
- const refresh = useCallback7(async () => {
1947
+ import { useCallback as useCallback10, useEffect as useEffect8, useRef as useRef7, useState as useState9 } from "react";
1948
+ function useSavedQueries(key, currentQuery, applyQueryState, storage, now, snapshot) {
1949
+ const [items, setItems] = useState9([]);
1950
+ const [defaultId, setDefaultId] = useState9(null);
1951
+ const [loading, setLoading] = useState9(true);
1952
+ const storageRef = useRef7(storage);
1953
+ storageRef.current = storage;
1954
+ const refresh = useCallback10(async () => {
1955
+ const storage2 = storageRef.current;
1224
1956
  setLoading(true);
1225
1957
  try {
1226
- const [savedItems, defaultSaved] = await Promise.all([
1227
- storage.listSaved(key),
1228
- storage.loadDefaultSaved?.(key) ?? Promise.resolve(null)
1958
+ const [savedItems, defaultSaved] = await Promise.allSettled([
1959
+ storage2.listSaved(key),
1960
+ storage2.loadDefaultSaved?.(key) ?? Promise.resolve(null)
1229
1961
  ]);
1230
- setItems(savedItems);
1231
- setDefaultId(defaultSaved?.id ?? null);
1962
+ if (savedItems.status === "fulfilled") setItems(savedItems.value);
1963
+ if (defaultSaved.status === "fulfilled") setDefaultId(defaultSaved.value?.id ?? null);
1232
1964
  } finally {
1233
1965
  setLoading(false);
1234
1966
  }
1235
- }, [storage, key]);
1236
- useEffect4(() => {
1967
+ }, [key]);
1968
+ useEffect8(() => {
1237
1969
  void refresh();
1238
1970
  }, [refresh]);
1239
- const save = useCallback7(
1971
+ const save = useCallback10(
1240
1972
  async (name) => {
1241
- const saved = await storage.saveNamed(key, name, currentQuery, now());
1973
+ name = name.trim();
1974
+ if (!name || name.length > 200) throw Error("Saved query name must be between 1 and 200 characters.");
1975
+ const immutable = await snapshot?.(currentQuery);
1976
+ const saved = await storage.saveNamed(key, name, currentQuery, now(), immutable?.code);
1242
1977
  await refresh();
1243
1978
  return saved;
1244
1979
  },
1245
- [storage, key, currentQuery, now, refresh]
1980
+ [storage, key, currentQuery, now, refresh, snapshot]
1246
1981
  );
1247
- const load = useCallback7(
1982
+ const load = useCallback10(
1248
1983
  (id) => {
1249
1984
  const found = items.find((q) => q.id === id);
1250
1985
  if (found) applyQueryState(found.query);
1251
1986
  },
1252
1987
  [items, applyQueryState]
1253
1988
  );
1254
- const setDefault = useCallback7(
1989
+ const update = useCallback10(
1990
+ async (id) => {
1991
+ if (!storage.updateNamed) throw Error("This storage adapter does not support saved-query updates.");
1992
+ const immutable = await snapshot?.(currentQuery);
1993
+ const saved = await storage.updateNamed(key, id, currentQuery, now(), immutable?.code);
1994
+ await refresh();
1995
+ return saved;
1996
+ },
1997
+ [storage, key, currentQuery, now, refresh, snapshot]
1998
+ );
1999
+ const setDefault = useCallback10(
1255
2000
  async (id) => {
1256
2001
  await storage.setDefaultSaved?.(key, id);
1257
2002
  await refresh();
1258
2003
  },
1259
2004
  [storage, key, refresh]
1260
2005
  );
1261
- const clearDefault = useCallback7(async () => {
2006
+ const clearDefault = useCallback10(async () => {
1262
2007
  await storage.setDefaultSaved?.(key, null);
1263
2008
  await refresh();
1264
2009
  }, [storage, key, refresh]);
1265
- const remove = useCallback7(
2010
+ const remove = useCallback10(
1266
2011
  async (id) => {
1267
2012
  if (id === defaultId) await storage.setDefaultSaved?.(key, null);
1268
2013
  await storage.deleteSaved(key, id);
@@ -1270,14 +2015,118 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
1270
2015
  },
1271
2016
  [storage, key, defaultId, refresh]
1272
2017
  );
1273
- return { items, loading, defaultId, save, load, setDefault, clearDefault, remove };
2018
+ return { items, loading, defaultId, save, canUpdate: Boolean(storage.updateNamed), update, load, setDefault, clearDefault, remove };
2019
+ }
2020
+
2021
+ // src/useQueryLinks.ts
2022
+ import { useCallback as useCallback11, useEffect as useEffect9, useRef as useRef8, useState as useState10 } from "react";
2023
+ import { encodeQuery } from "@pythia-software/query-table-core";
2024
+ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, onUrlChange) {
2025
+ const latest = useRef8(query);
2026
+ latest.current = query;
2027
+ const [kind, setKind] = useState10(null);
2028
+ const [busy, setBusy] = useState10(false);
2029
+ const [error, setError] = useState10(null);
2030
+ const mounted = useRef8(true);
2031
+ const operations = useRef8(0);
2032
+ useEffect9(() => {
2033
+ mounted.current = true;
2034
+ return () => {
2035
+ mounted.current = false;
2036
+ };
2037
+ }, []);
2038
+ const queryKey = JSON.stringify(query);
2039
+ useEffect9(() => {
2040
+ if (!syncUrl || !ready || typeof window === "undefined") return;
2041
+ try {
2042
+ const result = service.local(query, window.location.href, parameter);
2043
+ window.history.replaceState(window.history.state, "", result.url);
2044
+ onUrlChange(result.url);
2045
+ setKind(result.kind);
2046
+ setError(null);
2047
+ } catch (e) {
2048
+ setError(e instanceof Error ? e : Error(String(e)));
2049
+ }
2050
+ }, [queryKey, syncUrl, ready, service, parameter, onUrlChange]);
2051
+ const snapshot = useCallback11(async (q) => {
2052
+ const captured = encodeQuery(q);
2053
+ const base = typeof window !== "undefined" ? window.location.href : void 0;
2054
+ operations.current++;
2055
+ setBusy(true);
2056
+ setError(null);
2057
+ try {
2058
+ if (!ready) throw Error("Resolve or reset the query link before saving it.");
2059
+ const saved = await service.snapshot(q);
2060
+ if (saved && base && mounted.current && syncUrl && ready && window.location.href === base && encodeQuery(latest.current) === captured) {
2061
+ const url = new URL(base);
2062
+ url.searchParams.set(parameter, "@" + saved.code);
2063
+ const result = service.local(q, url.toString(), parameter);
2064
+ window.history.replaceState(window.history.state, "", result.url);
2065
+ onUrlChange(result.url);
2066
+ setKind("durable");
2067
+ }
2068
+ return saved;
2069
+ } catch (e) {
2070
+ if (mounted.current) setError(e instanceof Error ? e : Error(String(e)));
2071
+ throw e;
2072
+ } finally {
2073
+ operations.current--;
2074
+ if (mounted.current) setBusy(operations.current > 0);
2075
+ }
2076
+ }, [service, syncUrl, ready, parameter, onUrlChange]);
2077
+ const create = useCallback11(async (q = latest.current, options = {}) => {
2078
+ const baseUrl = options.baseUrl ?? window.location.href;
2079
+ const param = options.parameter ?? parameter;
2080
+ const captured = encodeQuery(q);
2081
+ operations.current++;
2082
+ setBusy(true);
2083
+ setError(null);
2084
+ try {
2085
+ if (!ready) throw Error("Resolve or reset the query link before sharing it.");
2086
+ const result = await service.durable(q, baseUrl, param);
2087
+ if (mounted.current && syncUrl && ready && param === parameter && window.location.href === baseUrl && encodeQuery(latest.current) === captured) {
2088
+ window.history.replaceState(window.history.state, "", result.url);
2089
+ onUrlChange(result.url);
2090
+ setKind(result.kind);
2091
+ }
2092
+ return result;
2093
+ } catch (e) {
2094
+ if (mounted.current) setError(e instanceof Error ? e : Error(String(e)));
2095
+ throw e;
2096
+ } finally {
2097
+ operations.current--;
2098
+ if (mounted.current) setBusy(operations.current > 0);
2099
+ }
2100
+ }, [service, syncUrl, ready, parameter, onUrlChange]);
2101
+ const local = useCallback11((q, base = window.location.href, param = parameter) => service.local(q, base, param), [service, parameter]);
2102
+ return { maxUrlBytes, kind, busy, error, create, local, snapshot };
1274
2103
  }
1275
2104
 
1276
2105
  // src/useQueryTable.ts
2106
+ import { createQueryLinkService } from "@pythia-software/query-table-core";
1277
2107
  var AUTOCOMPLETE_LIMIT = 50;
1278
2108
  var MAX_AUTO_REFRESH_POLLS = 1e3;
1279
- function defaultsFor(schema) {
1280
- return normalizeQueryState2({
2109
+ function detailColumnSchema(schema, enabled) {
2110
+ if (!enabled) return schema;
2111
+ return {
2112
+ ...schema,
2113
+ defaultSelect: [...schema.defaultSelect ?? selectedFields2(schema, { select: [] }).map((field) => ({ field: field.name })), { field: ITEM_DETAILS_COLUMN }],
2114
+ fields: [...schema.fields, {
2115
+ name: ITEM_DETAILS_COLUMN,
2116
+ label: "Detail",
2117
+ type: "text",
2118
+ source: { kind: "derived", accessor: () => null },
2119
+ select: { default: true, width: 112 },
2120
+ filter: { enabled: false },
2121
+ sort: { enabled: false },
2122
+ aggregate: { measure: false, groupable: false },
2123
+ property: { enabled: false }
2124
+ }]
2125
+ };
2126
+ }
2127
+ function defaultsFor(schema, details = false) {
2128
+ schema = detailColumnSchema(schema, details);
2129
+ return normalizeQueryState3({
1281
2130
  select: schema.defaultSelect ?? [],
1282
2131
  where: [],
1283
2132
  orderBy: schema.defaultSort ?? [],
@@ -1286,7 +2135,7 @@ function defaultsFor(schema) {
1286
2135
  });
1287
2136
  }
1288
2137
  function cloneWhereTerm(term) {
1289
- const copy = isOrGroup(term) ? { ...term, any: term.any.map((c) => ({ ...c })) } : { ...term };
2138
+ const copy = isOrGroup2(term) ? { ...term, any: term.any.map((c) => ({ ...c })) } : { ...term };
1290
2139
  if (term.setFilter)
1291
2140
  copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
1292
2141
  return copy;
@@ -1301,10 +2150,13 @@ function cloneQueryState(q) {
1301
2150
  limit: q.limit,
1302
2151
  offset: q.offset
1303
2152
  };
2153
+ if (q.fitToScreen) clone.fitToScreen = true;
1304
2154
  if (q.aggregations)
1305
2155
  clone.aggregations = JSON.parse(
1306
2156
  JSON.stringify(q.aggregations)
1307
2157
  );
2158
+ if (q.grouping) clone.grouping = normalizeQueryState3(q).grouping;
2159
+ if (q.itemDetails) clone.itemDetails = JSON.parse(JSON.stringify(q.itemDetails));
1308
2160
  return clone;
1309
2161
  }
1310
2162
  function mergeAggregation(a, patch) {
@@ -1353,18 +2205,25 @@ function distinctValuesFromRows(field, rows, search) {
1353
2205
  }
1354
2206
  return { values: values.sort((a, b) => a.localeCompare(b)), hasMore };
1355
2207
  }
2208
+ function hasQueryLink(url, parameter) {
2209
+ const values = new URL(url).searchParams.getAll(parameter);
2210
+ return values.length > 1 || values.length === 1 && values[0] !== "";
2211
+ }
2212
+ function queryParameter(opts) {
2213
+ return opts.queryParameter ?? (typeof opts.itemDetails === "object" ? opts.itemDetails.queryParameter : void 0) ?? "q";
2214
+ }
1356
2215
  function resolveInitial(opts) {
1357
- const defaults = defaultsFor(opts.schema);
2216
+ const defaults = defaultsFor(opts.schema, Boolean(opts.itemDetails));
1358
2217
  if (opts.initialQuery)
1359
2218
  return {
1360
- q: normalizeQueryState2(opts.initialQuery, defaults),
2219
+ q: normalizeQueryState3(opts.initialQuery, defaults),
1361
2220
  fromUrl: false
1362
2221
  };
1363
- if (opts.syncUrl === true && typeof window !== "undefined") {
1364
- const token = new URLSearchParams(window.location.search).get("q");
1365
- if (token)
2222
+ if (opts.syncUrl === true && opts.active !== false && typeof window !== "undefined") {
2223
+ const parameter = queryParameter(opts);
2224
+ if (hasQueryLink(window.location.href, parameter))
1366
2225
  return {
1367
- q: normalizeQueryState2(decodeQuery(token), defaults),
2226
+ q: defaults,
1368
2227
  fromUrl: true
1369
2228
  };
1370
2229
  }
@@ -1372,85 +2231,241 @@ function resolveInitial(opts) {
1372
2231
  }
1373
2232
  function useQueryTable(opts) {
1374
2233
  const {
1375
- schema,
2234
+ schema: sourceSchema,
1376
2235
  transport: sourceTransport,
1377
- clientRows,
2236
+ clientRows: providedRows,
1378
2237
  initialQuery,
1379
- syncUrl = false,
1380
2238
  debounceMs = 200
1381
2239
  } = opts;
1382
- const { transport, activity: requestActivity } = useRequestActivity(sourceTransport);
1383
- const storage = useMemo7(
2240
+ const active = opts.active !== false;
2241
+ const urlSyncEnabled = opts.syncUrl === true;
2242
+ const syncUrl = urlSyncEnabled && active;
2243
+ const { transport: metadataTransport, activity: requestActivity } = useRequestActivity(sourceTransport);
2244
+ const transport = metadataTransport;
2245
+ const storage = useMemo9(
1384
2246
  () => opts.storage ?? memoryStorageAdapter(),
1385
2247
  [opts.storage]
1386
2248
  );
1387
- const now = opts.now ?? Date.now;
1388
- const nowRef = useRef5(now);
1389
- const onRefreshRef = useRef5(opts.onRefresh);
1390
- const defaults = useMemo7(() => defaultsFor(schema), [schema]);
1391
- const byName = useMemo7(
1392
- () => new Map(schema.fields.map((f) => [f.name, f])),
1393
- [schema]
2249
+ const parameter = queryParameter(opts);
2250
+ const linkService = useMemo9(
2251
+ () => createQueryLinkService(sourceSchema.name, storage.snapshots, opts.links),
2252
+ [sourceSchema.name, storage.snapshots, opts.links?.cache, opts.links?.maxUrlBytes]
1394
2253
  );
1395
- const initRef = useRef5(resolveInitial(opts));
1396
- const aggIdRef = useRef5(0);
1397
- const [storedQuery, setQueryState] = useState7(initRef.current.q);
1398
- const query = useMemo7(
2254
+ const now = opts.now ?? Date.now;
2255
+ const nowRef = useRef9(now);
2256
+ const onRefreshRef = useRef9(opts.onRefresh);
2257
+ const defaults = useMemo9(() => defaultsFor(sourceSchema, Boolean(opts.itemDetails)), [sourceSchema, Boolean(opts.itemDetails)]);
2258
+ const initRef = useRef9(resolveInitial(opts));
2259
+ const [restoration, setRestoration] = useState11({ loading: initRef.current.fromUrl, error: null });
2260
+ const [storageReady, setStorageReady] = useState11(Boolean(initialQuery));
2261
+ const restorationCurrent = useRef9({ storageReady, restoration, active });
2262
+ restorationCurrent.current = { storageReady, restoration, active };
2263
+ const manualQueryEpoch = useRef9(0);
2264
+ const restoreController = useRef9(null);
2265
+ const restoreHandler = useRef9(null);
2266
+ const initialRestore = useRef9(null);
2267
+ const initialRestorationStarted = useRef9(false);
2268
+ const restoreLocation = useCallback12((location, signal) => {
2269
+ if (!restoreHandler.current) return Promise.reject(Error("Query controller is not mounted."));
2270
+ return restoreHandler.current(String(location), signal);
2271
+ }, []);
2272
+ const aggIdRef = useRef9(0);
2273
+ const savedBaseline = useRef9(
2274
+ initRef.current.q.grouping ? { source: sourceSchema.name, query: cloneQueryState(initRef.current.q) } : null
2275
+ );
2276
+ const viewRevision = useRef9(0);
2277
+ const [queryHistory, setQueryHistory] = useState11(
2278
+ () => ({ query: initRef.current.q, past: [], future: [] })
2279
+ );
2280
+ const queryHistoryRef = useRef9(queryHistory);
2281
+ const updateQueryHistory = useCallback12((update) => {
2282
+ const previous = queryHistoryRef.current;
2283
+ const next = update(previous);
2284
+ if (next === previous) return;
2285
+ queryHistoryRef.current = next;
2286
+ setQueryHistory(next);
2287
+ }, []);
2288
+ const storedQuery = queryHistory.query;
2289
+ const [nonce, setNonce] = useState11(0);
2290
+ const query = useMemo9(
1399
2291
  () => opts.canonicalizeQuery?.(storedQuery) ?? storedQuery,
1400
2292
  [storedQuery, opts.canonicalizeQuery]
1401
2293
  );
1402
- const undoStack = useRef5([cloneQueryState(initRef.current.q)]);
1403
- const redoStack = useRef5([]);
1404
- const [rows, setRows] = useState7([]);
1405
- const [serverComputed, setServerComputed] = useState7(/* @__PURE__ */ new Map());
1406
- const [committedFields, setCommittedFields] = useState7([]);
1407
- const [serverExecution, setServerExecution] = useState7();
1408
- const [total, setTotal] = useState7(null);
1409
- const [loading, setLoading] = useState7(false);
1410
- const [error, setError] = useState7(null);
1411
- const [nonce, setNonce] = useState7(0);
1412
- const [committedPage, setCommittedPage] = useState7(null);
1413
- const [committedSource, setCommittedSource] = useState7(null);
1414
- const [autoRefreshStatus, setAutoRefreshStatus] = useState7(null);
1415
- useEffect5(() => {
2294
+ const sourceCatalogue = useMemo9(() => sourceExecutionSchema(sourceSchema, opts.computedExecution), [sourceSchema, opts.computedExecution]);
2295
+ const sourceRows = providedRows;
2296
+ const groupingModel = useMemo9(() => createGroupingModel(sourceCatalogue), [sourceCatalogue]);
2297
+ const remoteGrouping = !!transport?.fetchQueryV3 && transport.queryCapabilities?.version === 3 && transport.queryCapabilities.groupedRows;
2298
+ const groupingSupported = (!transport && sourceRows !== void 0 || remoteGrouping) && opts.grouping !== false;
2299
+ const groupKey = JSON.stringify(query.grouping ?? null);
2300
+ const groupingNow = useMemo9(
2301
+ () => query.grouping ? (opts.evaluationNow ?? Date.now)() : void 0,
2302
+ [sourceRows, sourceSchema, groupingSupported, groupKey, nonce, opts.evaluationNow]
2303
+ );
2304
+ const prepared = useMemo9(() => {
2305
+ if (!query.grouping) return null;
2306
+ try {
2307
+ if (!groupingSupported) throw new Error("Grouped queries require a server with grouped-query support or a dataset already in memory.");
2308
+ if (remoteGrouping) {
2309
+ groupingModel.validate(query.grouping);
2310
+ return { inputs: [], rows: [], error: null };
2311
+ }
2312
+ return { ...prepareGrouping(sourceRows, sourceCatalogue, query.grouping, { now: groupingNow }), error: null };
2313
+ } catch (error2) {
2314
+ return { inputs: [], rows: [], error: error2 };
2315
+ }
2316
+ }, [sourceRows, sourceCatalogue, groupingModel, groupingSupported, groupKey, nonce, groupingNow, remoteGrouping]);
2317
+ const projectionKey = JSON.stringify(query.grouping ? [query.grouping.keys, query.grouping.summaries] : null);
2318
+ const schema = useMemo9(() => query.grouping ? groupingModel.resultSchema(query.grouping.keys, query.grouping.summaries) : sourceSchema, [sourceSchema, groupingModel, projectionKey]);
2319
+ const clientRows = prepared?.rows ?? sourceRows;
2320
+ const executionQuery = useMemo9(() => withoutGrouping2(query), [query]);
2321
+ const byName = useMemo9(() => new Map(schema.fields.map((field) => [field.name, field])), [schema]);
2322
+ const detailsEnabled = Boolean(opts.itemDetails) && !query.grouping;
2323
+ const validateExecution = useCallback12((q) => {
2324
+ if (prepared?.error) throw prepared.error;
2325
+ opts.validateQuery?.(query.grouping ? { ...q, grouping: query.grouping } : q);
2326
+ }, [prepared?.error, opts.validateQuery, groupKey]);
2327
+ const [committedRows, setRows] = useState11([]);
2328
+ const [serverComputed, setServerComputed] = useState11(/* @__PURE__ */ new Map());
2329
+ const [committedFields, setCommittedFields] = useState11([]);
2330
+ const [serverExecution, setServerExecution] = useState11();
2331
+ const [total, setTotal] = useState11(null);
2332
+ const [loading, setLoading] = useState11(false);
2333
+ const [error, setError] = useState11(null);
2334
+ const [committedPage, setCommittedPage] = useState11(null);
2335
+ const [committedSource, setCommittedSource] = useState11(null);
2336
+ const rows = useMemo9(() => committedSource?.schema === schema ? committedRows : [], [committedSource?.schema, schema, committedRows]);
2337
+ const [autoRefreshStatus, setAutoRefreshStatus] = useState11(null);
2338
+ useEffect10(() => {
1416
2339
  nowRef.current = now;
1417
2340
  }, [now]);
1418
- useEffect5(() => {
2341
+ useEffect10(() => {
1419
2342
  onRefreshRef.current = opts.onRefresh;
1420
2343
  }, [opts.onRefresh]);
1421
- const setQuery = useCallback8(
2344
+ const setQuery = useCallback12(
1422
2345
  (next) => {
1423
- setQueryState((prev) => {
2346
+ manualQueryEpoch.current++;
2347
+ initialRestorationStarted.current = true;
2348
+ restoreController.current?.abort();
2349
+ setStorageReady(true);
2350
+ setRestoration((previous) => !previous.loading && !previous.error ? previous : { loading: false, error: null });
2351
+ updateQueryHistory((history) => {
2352
+ const prev = history.query;
1424
2353
  const candidate = typeof next === "function" ? next(prev) : next;
1425
- const nextQuery = normalizeQueryState2(candidate, defaults);
1426
- if (queriesEqual(prev, nextQuery)) return prev;
1427
- undoStack.current.push(cloneQueryState(prev));
1428
- redoStack.current = [];
1429
- return cloneQueryState(nextQuery);
2354
+ const nextQuery = normalizeQueryState3(candidate, defaults);
2355
+ if (queriesEqual2(prev, nextQuery)) return history;
2356
+ return { query: cloneQueryState(nextQuery), past: [...history.past, cloneQueryState(prev)], future: [] };
1430
2357
  });
1431
2358
  },
1432
- [defaults]
1433
- );
1434
- useEffect5(() => {
1435
- if (initRef.current.fromUrl || initialQuery) return;
2359
+ [defaults, updateQueryHistory]
2360
+ );
2361
+ const sourceQuery = query.grouping?.source ?? query;
2362
+ const setSourceQuery = useCallback12((next) => {
2363
+ setQuery((current) => {
2364
+ const previous = current.grouping?.source ?? current;
2365
+ const source = withoutGrouping2(typeof next === "function" ? next(previous) : next);
2366
+ return current.grouping ? { ...current, offset: 0, grouping: { ...current.grouping, source } } : source;
2367
+ });
2368
+ }, [setQuery]);
2369
+ const sourceFilters = useQueryFilters(sourceCatalogue, setSourceQuery);
2370
+ const observedQueryUrl = useRef9(null);
2371
+ const recordQueryUrl = useCallback12((url) => {
2372
+ observedQueryUrl.current = JSON.stringify(new URL(url).searchParams.getAll(parameter));
2373
+ }, [parameter]);
2374
+ const links = useQueryLinks(
2375
+ query,
2376
+ linkService,
2377
+ syncUrl,
2378
+ storageReady && !restoration.loading && !restoration.error,
2379
+ parameter,
2380
+ opts.links?.maxUrlBytes ?? 2e3,
2381
+ recordQueryUrl
2382
+ );
2383
+ const restoreSources = useRef9({ storage, linkService, defaults, initialQuery });
2384
+ restoreSources.current = { storage, linkService, defaults, initialQuery };
2385
+ useEffect10(() => {
1436
2386
  let cancelled = false;
1437
- void (async () => {
1438
- const defaultSaved = await storage.loadDefaultSaved?.(schema.name);
1439
- const restored = defaultSaved?.query ?? await storage.loadLast(schema.name);
1440
- if (!cancelled && restored) {
1441
- const normalized = normalizeQueryState2(restored, defaults);
1442
- undoStack.current = [cloneQueryState(normalized)];
1443
- redoStack.current = [];
1444
- setQueryState(normalized);
2387
+ let controller = new AbortController();
2388
+ let generation = 0;
2389
+ initialRestorationStarted.current = Boolean(restoreSources.current.initialQuery);
2390
+ async function restore(navigation = false, location = typeof window !== "undefined" ? window.location.href : "", signal) {
2391
+ initialRestorationStarted.current = true;
2392
+ const { storage: storage2, linkService: linkService2, defaults: defaults2 } = restoreSources.current;
2393
+ const run = ++generation;
2394
+ const epoch = manualQueryEpoch.current;
2395
+ controller.abort();
2396
+ controller = new AbortController();
2397
+ restoreController.current = controller;
2398
+ const currentController = controller;
2399
+ const abort2 = () => currentController.abort();
2400
+ signal?.addEventListener("abort", abort2, { once: true });
2401
+ if (signal?.aborted) currentController.abort();
2402
+ setStorageReady(false);
2403
+ const explicitLink = !!location && (navigation || urlSyncEnabled) && hasQueryLink(location, parameter);
2404
+ setRestoration({ loading: true, error: null });
2405
+ try {
2406
+ if (currentController.signal.aborted) throw new DOMException("Query restoration cancelled.", "AbortError");
2407
+ const fromUrl = location && (navigation || urlSyncEnabled) ? await linkService2.resolve(location, parameter, currentController.signal) : null;
2408
+ const defaultSaved = fromUrl ? null : await storage2.loadDefaultSaved?.(sourceSchema.name);
2409
+ const restored = fromUrl ?? defaultSaved?.query ?? (navigation ? defaults2 : await storage2.loadLast(sourceSchema.name));
2410
+ if (currentController.signal.aborted) throw new DOMException("Query restoration cancelled.", "AbortError");
2411
+ if (!cancelled && run === generation && epoch === manualQueryEpoch.current) {
2412
+ if (restored) {
2413
+ const normalized = normalizeQueryState3(restored, defaults2);
2414
+ savedBaseline.current = defaultSaved || fromUrl?.grouping ? { source: sourceSchema.name, query: cloneQueryState(normalized) } : null;
2415
+ viewRevision.current++;
2416
+ updateQueryHistory(() => ({ query: normalized, past: [], future: [] }));
2417
+ }
2418
+ setStorageReady(true);
2419
+ setRestoration((previous) => !previous.loading && !previous.error ? previous : { loading: false, error: null });
2420
+ }
2421
+ } catch (e) {
2422
+ if (!cancelled && run === generation && epoch === manualQueryEpoch.current) {
2423
+ if (explicitLink || navigation && currentController.signal.aborted) {
2424
+ setRestoration({ loading: false, error: e instanceof Error ? e : Error(String(e)) });
2425
+ } else {
2426
+ updateQueryHistory(() => ({ query: defaults2, past: [], future: [] }));
2427
+ setRestoration({ loading: false, error: null });
2428
+ }
2429
+ setStorageReady(true);
2430
+ }
2431
+ if (navigation && !cancelled && run === generation && epoch === manualQueryEpoch.current) throw e;
2432
+ } finally {
2433
+ signal?.removeEventListener("abort", abort2);
1445
2434
  }
1446
- })();
2435
+ }
2436
+ restoreHandler.current = async (location, signal) => {
2437
+ location = new URL(location, typeof window !== "undefined" ? window.location.href : void 0).href;
2438
+ const key = JSON.stringify(new URL(location).searchParams.getAll(parameter));
2439
+ const current = restorationCurrent.current;
2440
+ if (key === observedQueryUrl.current && current.storageReady && !current.restoration.loading && !current.restoration.error) return;
2441
+ recordQueryUrl(location);
2442
+ await restore(true, location, signal);
2443
+ };
2444
+ initialRestore.current = () => restore();
2445
+ if (typeof window !== "undefined") recordQueryUrl(window.location.href);
2446
+ const navigate = () => {
2447
+ if (!restorationCurrent.current.active) return;
2448
+ const key = JSON.stringify(new URL(window.location.href).searchParams.getAll(parameter));
2449
+ if (key === observedQueryUrl.current) return;
2450
+ observedQueryUrl.current = key;
2451
+ void restore(true).catch(() => {
2452
+ });
2453
+ };
2454
+ if (urlSyncEnabled && typeof window !== "undefined") window.addEventListener("popstate", navigate);
1447
2455
  return () => {
1448
2456
  cancelled = true;
2457
+ controller.abort();
2458
+ restoreHandler.current = null;
2459
+ initialRestore.current = null;
2460
+ if (urlSyncEnabled && typeof window !== "undefined") window.removeEventListener("popstate", navigate);
1449
2461
  };
1450
- }, []);
2462
+ }, [sourceSchema.name, urlSyncEnabled, parameter, recordQueryUrl, updateQueryHistory]);
2463
+ useEffect10(() => {
2464
+ if (active && !initialRestorationStarted.current) void initialRestore.current?.();
2465
+ }, [active, sourceSchema.name, urlSyncEnabled, parameter, recordQueryUrl]);
1451
2466
  const { api: computed, displaySchema } = useComputedColumns({
1452
2467
  schema,
1453
- query,
2468
+ query: executionQuery,
1454
2469
  rows,
1455
2470
  serverComputed,
1456
2471
  ...serverExecution ? { serverExecution } : {},
@@ -1460,11 +2475,11 @@ function useQueryTable(opts) {
1460
2475
  ...transport ? { transport } : {},
1461
2476
  ...clientRows ? { clientRows } : {}
1462
2477
  });
1463
- const resolveComputed = useCallback8(
2478
+ const resolveComputed = useCallback12(
1464
2479
  (name) => isComputedField4(name) ? computed.compile(`[${name.replace(/]/g, "]] ".trim())}]`) : void 0,
1465
2480
  [computed.compile]
1466
2481
  );
1467
- const metricCompile = useCallback8(
2482
+ const metricCompile = useCallback12(
1468
2483
  (clause) => {
1469
2484
  const diagnostics = validateMetric2(
1470
2485
  clause,
@@ -1477,19 +2492,29 @@ function useQueryTable(opts) {
1477
2492
  },
1478
2493
  [displaySchema, resolveComputed]
1479
2494
  );
1480
- const computedRevisions = useMemo7(
2495
+ const computedRevisions = useMemo9(
1481
2496
  () => Object.fromEntries(computed.definitions.map((d) => [d.id, d.revision])),
1482
2497
  [computed.definitions]
1483
2498
  );
1484
2499
  const definitionKey = JSON.stringify(computedRevisions);
1485
- const queryKey = useMemo7(() => encodeQuery(query), [query]);
1486
- const serverRowQuery = useMemo7(() => {
2500
+ const queryKey = useMemo9(() => JSON.stringify(query), [query]);
2501
+ const serverRowQuery = useMemo9(() => {
1487
2502
  const execution = opts.computedExecution;
1488
- const v2Request = execution && transport?.fetchRowsV2 ? toServerQueryV2(query, displaySchema, execution) : void 0;
1489
- const request = v2Request ?? toServerQuery2(query, displaySchema);
2503
+ const detailConfig = typeof opts.itemDetails === "object" ? opts.itemDetails : void 0;
2504
+ const page = query.itemDetails?.page ?? detailConfig?.page;
2505
+ const pageEnabled = Boolean(opts.itemDetails) && (query.itemDetails?.modes ?? initialDetailModes(detailConfig)).includes("page") && (!detailConfig?.modes || detailConfig.modes.includes("page"));
2506
+ const pageFields = pageEnabled ? itemPageFields(page?.urlTemplate ?? "") : [];
2507
+ const requestQuery = pageFields.length ? { ...executionQuery, select: [
2508
+ ...query.select.length ? query.select : defaults.select,
2509
+ ...pageFields.filter((name) => displaySchema.fields.some((field) => field.name === name)).map((field) => ({ field }))
2510
+ ] } : executionQuery;
2511
+ const v2Request = execution && transport?.fetchRowsV2 ? toServerQueryV2(requestQuery, displaySchema, execution) : void 0;
2512
+ const request = v2Request ?? toServerQuery2(requestQuery, displaySchema);
1490
2513
  if (v2Request)
1491
2514
  for (const definition of computed.definitions) {
1492
- if (executionRevision(execution, definition.id) !== void 0 && executionRevision(execution, definition.id) !== definition.revision)
2515
+ if ((execution?.fields[computedFieldName2(definition.id)]?.select || v2Request.orderBy.some(
2516
+ (term) => term.field === computedFieldName2(definition.id)
2517
+ )) && executionRevision(execution, definition.id) !== void 0 && executionRevision(execution, definition.id) !== definition.revision)
1493
2518
  v2Request.diagnostics.push({
1494
2519
  from: 0,
1495
2520
  to: 1,
@@ -1523,13 +2548,26 @@ function useQueryTable(opts) {
1523
2548
  computedRevisions,
1524
2549
  schema,
1525
2550
  resolveComputed,
1526
- opts.computedExecution
2551
+ opts.computedExecution,
2552
+ opts.itemDetails,
2553
+ defaults
1527
2554
  ]);
1528
- const rowQueryKey = useMemo7(
1529
- () => transport ? `server:${JSON.stringify(serverRowQuery)}` : `client:${JSON.stringify([query.where, query.orderBy, query.limit, query.offset])}`,
2555
+ const rowQueryKey = useMemo9(
2556
+ () => transport ? `server:${JSON.stringify(serverRowQuery)}:${transport.fetchQueryV3 ? JSON.stringify([
2557
+ groupKey,
2558
+ groupingNow,
2559
+ query.select.map((column) => column.field),
2560
+ query.where,
2561
+ query.orderBy,
2562
+ query.limit,
2563
+ query.offset
2564
+ ]) : ""}` : `client:${JSON.stringify([query.where, query.orderBy, query.limit, query.offset])}`,
1530
2565
  [
1531
2566
  transport,
1532
2567
  serverRowQuery,
2568
+ groupKey,
2569
+ groupingNow,
2570
+ query.select,
1533
2571
  query.where,
1534
2572
  query.orderBy,
1535
2573
  query.limit,
@@ -1538,15 +2576,50 @@ function useQueryTable(opts) {
1538
2576
  );
1539
2577
  const executionKey = JSON.stringify(opts.computedExecution ?? null);
1540
2578
  const pageContext = `${schema.name}:${rowQueryKey}:${nonce}:${executionKey}`;
1541
- useEffect5(() => {
2579
+ useEffect10(() => {
2580
+ if (!active || !storageReady || restoration.loading || restoration.error) {
2581
+ setRows((previous) => previous.length ? [] : previous);
2582
+ setTotal(null);
2583
+ setCommittedPage(null);
2584
+ setLoading(false);
2585
+ return;
2586
+ }
1542
2587
  const ac = new AbortController();
1543
2588
  let cancelled = false;
1544
2589
  const run = async () => {
1545
2590
  setLoading(true);
1546
2591
  setError(null);
1547
2592
  try {
1548
- opts.validateQuery?.(query);
2593
+ validateExecution(query);
1549
2594
  if (transport) {
2595
+ if (transport.fetchQueryV3 && transport.queryCapabilities?.version === 3 && (query.grouping || query.select.some((column) => isComputedField4(column.field)) || query.orderBy.some((term) => isComputedField4(term.field)) || query.where.some((term) => ("any" in term ? term.any : [term]).some((predicate) => isComputedField4(predicate.field))))) {
2596
+ const request = toQueryRequestV3(
2597
+ { ...query, aggregations: [], select: [
2598
+ ...query.select.filter((column) => isComputedField4(column.field)),
2599
+ ...serverRowQuery.select.map((field) => ({ field }))
2600
+ ] },
2601
+ sourceSchema,
2602
+ transport.queryCapabilities,
2603
+ opts.computedExecution,
2604
+ { now: groupingNow ?? (opts.evaluationNow ?? Date.now)() }
2605
+ );
2606
+ if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
2607
+ const result = await transport.fetchQueryV3(request, ac.signal);
2608
+ validateQueryResultV3(result, request, query.grouping ? schema : sourceCatalogue);
2609
+ if (!cancelled) {
2610
+ setCommittedFields(request.result.select);
2611
+ setServerExecution(result.execution);
2612
+ setServerComputed(new Map(result.rows.map((row) => [
2613
+ row[schema.idField],
2614
+ Object.fromEntries(Object.entries(row).filter(([name]) => isComputedField4(name)).map(([name, value]) => [name.slice(10), value && typeof value === "object" && "computedError" in value ? { value: null, error: { code: String(value.computedError) } } : { value }]))
2615
+ ])));
2616
+ setRows(result.rows);
2617
+ setTotal(result.total);
2618
+ setCommittedPage(pageContext);
2619
+ setCommittedSource({ schema, transport, clientRows });
2620
+ }
2621
+ return;
2622
+ }
1550
2623
  const useV2 = "version" in serverRowQuery && transport.fetchRowsV2;
1551
2624
  if ("diagnostics" in serverRowQuery && Array.isArray(serverRowQuery.diagnostics) && serverRowQuery.diagnostics.length)
1552
2625
  throw Error(
@@ -1582,7 +2655,12 @@ function useQueryTable(opts) {
1582
2655
  setCommittedSource({ schema, transport, clientRows });
1583
2656
  }
1584
2657
  } else {
1585
- const res = applyQuery2(clientRows ?? [], query, schema);
2658
+ const res = applyQuery2(
2659
+ clientRows ?? [],
2660
+ executionQuery,
2661
+ schema,
2662
+ groupingNow !== void 0 ? { now: groupingNow } : {}
2663
+ );
1586
2664
  if (!cancelled) {
1587
2665
  setRows(res.rows);
1588
2666
  setTotal(res.total);
@@ -1614,58 +2692,107 @@ function useQueryTable(opts) {
1614
2692
  schema,
1615
2693
  debounceMs,
1616
2694
  nonce,
1617
- opts.validateQuery,
1618
- executionKey
2695
+ validateExecution,
2696
+ executionKey,
2697
+ groupingNow,
2698
+ restoration,
2699
+ active,
2700
+ storageReady
1619
2701
  ]);
1620
- useEffect5(() => {
1621
- if (!syncUrl || typeof window === "undefined") return;
1622
- const url = new URL(window.location.href);
1623
- const token = encodeQuery(query);
1624
- if (token) url.searchParams.set("q", token);
1625
- else url.searchParams.delete("q");
1626
- window.history.replaceState(null, "", url.toString());
1627
- }, [queryKey, syncUrl, query]);
1628
- useEffect5(() => {
1629
- const t = setTimeout(() => void storage.saveLast(schema.name, query), 400);
2702
+ useEffect10(() => {
2703
+ if (!storageReady || restoration.loading || restoration.error) return;
2704
+ const t = setTimeout(() => void storage.saveLast(sourceSchema.name, query), 400);
1630
2705
  return () => clearTimeout(t);
1631
- }, [queryKey, storage, schema.name, query]);
1632
- const idField = useMemo7(
2706
+ }, [queryKey, storage, sourceSchema.name, query, restoration, storageReady]);
2707
+ const idField = useMemo9(
1633
2708
  () => schema.fields.find((f) => f.name === schema.idField),
1634
2709
  [schema]
1635
2710
  );
1636
- const rowId = useCallback8(
1637
- (row) => idField ? readFieldValue4(idField, row) : null,
1638
- [idField]
2711
+ const rowId = useCallback12(
2712
+ (row) => (() => {
2713
+ const value = idField ? readFieldValue4(idField, row) : row?.[schema.idField];
2714
+ return typeof value === "string" || typeof value === "number" && Number.isFinite(value) ? value : null;
2715
+ })(),
2716
+ [idField, schema.idField]
1639
2717
  );
1640
- const displayedIds = useMemo7(
2718
+ const displayedIds = useMemo9(
1641
2719
  () => rows.map(rowId).filter((id) => id != null),
1642
2720
  [rows, rowId]
1643
2721
  );
1644
2722
  const selection = useSelection(displayedIds);
1645
- const select = useSelect(query, setQuery, displaySchema);
2723
+ const identityScope = JSON.stringify([sourceSchema.name, query.grouping?.keys ?? null]);
2724
+ useEffect10(() => {
2725
+ selection.clear();
2726
+ }, [identityScope, selection.clear]);
2727
+ const columnSchema = useMemo9(() => detailColumnSchema(displaySchema, detailsEnabled), [displaySchema, detailsEnabled]);
2728
+ const resultDefaults = useMemo9(() => query.grouping ? defaultsFor(schema) : defaults, [Boolean(query.grouping), schema, defaults]);
2729
+ const select = useSelect(query, setQuery, columnSchema);
1646
2730
  const columnDrag = useColumnDrag();
1647
- const saved = useSavedQueries(
1648
- schema.name,
2731
+ const savedQueries = useSavedQueries(
2732
+ sourceSchema.name,
1649
2733
  query,
1650
- (q) => setQuery(q),
2734
+ (q) => {
2735
+ const normalized = normalizeQueryState3(q, defaults);
2736
+ savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(normalized) };
2737
+ viewRevision.current++;
2738
+ setQuery(normalized);
2739
+ },
1651
2740
  storage,
1652
- now
1653
- );
2741
+ now,
2742
+ links.snapshot
2743
+ );
2744
+ const saved = { ...savedQueries, save: async (name) => {
2745
+ const item = await savedQueries.save(name);
2746
+ savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(normalizeQueryState3(item.query, defaults)) };
2747
+ viewRevision.current++;
2748
+ return item;
2749
+ } };
1654
2750
  const pageReady = committedPage === pageContext && committedSource?.schema === schema && committedSource?.transport === transport && committedSource?.clientRows === clientRows && !error;
1655
- const dataToken = useRef5(0);
1656
- const dataVersion = useMemo7(
2751
+ const dataToken = useRef9(0);
2752
+ const dataVersion = useMemo9(
1657
2753
  () => ++dataToken.current,
1658
2754
  [schema, transport, clientRows, nonce, definitionKey, executionKey]
1659
2755
  );
2756
+ const executeV3Metrics = useCallback12(async (metricQuery, signal) => {
2757
+ if (!transport?.fetchQueryV3 || transport.queryCapabilities?.version !== 3) throw Error("The v3 query executor is unavailable.");
2758
+ const request = toQueryRequestV3(
2759
+ query.grouping ? { ...metricQuery, grouping: query.grouping } : metricQuery,
2760
+ sourceSchema,
2761
+ transport.queryCapabilities,
2762
+ opts.computedExecution,
2763
+ { now: groupingNow ?? (opts.evaluationNow ?? Date.now)(), rows: false }
2764
+ );
2765
+ if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
2766
+ const result = await transport.fetchQueryV3(request, signal);
2767
+ validateQueryResultV3(result, request, query.grouping ? schema : sourceCatalogue);
2768
+ return { metrics: result.metrics };
2769
+ }, [transport, sourceSchema, sourceCatalogue, schema, opts.computedExecution, groupKey, groupingNow, opts.evaluationNow]);
2770
+ const previewGrouping = useCallback12(async (draft, signal) => {
2771
+ if (!active || !storageReady || restoration.loading || restoration.error) throw Error("Wait for query restoration before previewing groups.");
2772
+ if (!transport?.fetchQueryV3 || !transport.queryCapabilities?.groupedRows || !draft.grouping) throw Error("The server group preview is unavailable.");
2773
+ const request = toQueryRequestV3(
2774
+ { ...draft, aggregations: [] },
2775
+ sourceSchema,
2776
+ transport.queryCapabilities,
2777
+ opts.computedExecution,
2778
+ { now: groupingNow ?? (opts.evaluationNow ?? Date.now)() }
2779
+ );
2780
+ if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
2781
+ const result = await transport.fetchQueryV3(request, signal);
2782
+ validateQueryResultV3(result, request, groupingModel.resultSchema(draft.grouping.keys, draft.grouping.summaries));
2783
+ return { rows: result.rows, total: result.total, sourceTotal: result.sourceTotal };
2784
+ }, [transport, sourceSchema, opts.computedExecution, groupingModel, groupingNow, opts.evaluationNow, active, storageReady, restoration.loading, restoration.error]);
1660
2785
  const aggState = useAggregations(
1661
- query,
2786
+ executionQuery,
1662
2787
  schema,
1663
2788
  transport,
1664
2789
  clientRows,
1665
2790
  debounceMs,
1666
2791
  nonce,
1667
- opts.validateQuery,
2792
+ validateExecution,
1668
2793
  {
2794
+ ...transport?.fetchQueryV3 && transport.queryCapabilities?.version === 3 && (query.grouping || query.aggregations?.some((metric) => metric.dateGrouping) || query.where.some((term) => ("any" in term ? term.any : [term]).some((predicate) => isComputedField4(predicate.field)))) ? { executeV3: executeV3Metrics, queryContextKey: groupKey } : {},
2795
+ ...groupingNow !== void 0 ? { now: groupingNow, strictLocalMetrics: true } : {},
1669
2796
  shownRows: rows,
1670
2797
  rowsReady: pageReady,
1671
2798
  rowsError: error,
@@ -1676,11 +2803,12 @@ function useQueryTable(opts) {
1676
2803
  execution: pageReady && serverExecution ? serverExecution : opts.computedExecution
1677
2804
  } : {},
1678
2805
  ...opts.formulaWorkerFactory ? { workerFactory: opts.formulaWorkerFactory } : {}
1679
- }
2806
+ },
2807
+ active && storageReady && !restoration.loading && !restoration.error
1680
2808
  );
1681
- const queryRef = useRef5(query);
2809
+ const queryRef = useRef9(query);
1682
2810
  queryRef.current = query;
1683
- const replaceAggregations = useCallback8(
2811
+ const replaceAggregations = useCallback12(
1684
2812
  (clauses, expected) => {
1685
2813
  if (expected && JSON.stringify(queryRef.current.aggregations ?? []) !== JSON.stringify(expected))
1686
2814
  throw new Error(
@@ -1696,141 +2824,46 @@ function useQueryTable(opts) {
1696
2824
  },
1697
2825
  [setQuery, metricCompile]
1698
2826
  );
1699
- const canUndo = undoStack.current.length > 0;
1700
- const canRedo = redoStack.current.length > 0;
1701
- const undo = useCallback8(() => {
1702
- setQueryState((prev) => {
1703
- const previous = undoStack.current.pop();
1704
- if (!previous) return prev;
1705
- redoStack.current.push(cloneQueryState(prev));
1706
- return cloneQueryState(previous);
2827
+ const canUndo = queryHistory.past.length > 0;
2828
+ const canRedo = queryHistory.future.length > 0;
2829
+ const undo = useCallback12(() => {
2830
+ if (!queryHistoryRef.current.past.length) return;
2831
+ manualQueryEpoch.current++;
2832
+ restoreController.current?.abort();
2833
+ setStorageReady(true);
2834
+ setRestoration({ loading: false, error: null });
2835
+ updateQueryHistory((history) => {
2836
+ const previous = history.past[history.past.length - 1];
2837
+ if (!previous) return history;
2838
+ return { query: cloneQueryState(previous), past: history.past.slice(0, -1), future: [...history.future, cloneQueryState(history.query)] };
1707
2839
  });
1708
- }, []);
1709
- const redo = useCallback8(() => {
1710
- setQueryState((prev) => {
1711
- const next = redoStack.current.pop();
1712
- if (!next) return prev;
1713
- undoStack.current.push(cloneQueryState(prev));
1714
- return cloneQueryState(next);
2840
+ }, [updateQueryHistory]);
2841
+ const redo = useCallback12(() => {
2842
+ if (!queryHistoryRef.current.future.length) return;
2843
+ manualQueryEpoch.current++;
2844
+ restoreController.current?.abort();
2845
+ setStorageReady(true);
2846
+ setRestoration({ loading: false, error: null });
2847
+ updateQueryHistory((history) => {
2848
+ const next = history.future[history.future.length - 1];
2849
+ if (!next) return history;
2850
+ return { query: cloneQueryState(next), past: [...history.past, cloneQueryState(history.query)], future: history.future.slice(0, -1) };
1715
2851
  });
1716
- }, []);
1717
- const patch = useCallback8(
2852
+ }, [updateQueryHistory]);
2853
+ const patch = useCallback12(
1718
2854
  (p) => setQuery((prev) => ({ ...prev, ...p })),
1719
2855
  [setQuery]
1720
2856
  );
1721
- const addFilter = useCallback8(
1722
- (clause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })),
1723
- [setQuery]
1724
- );
1725
- const updateFilter = useCallback8(
1726
- (index, clause) => setQuery((q) => ({
1727
- ...q,
1728
- offset: 0,
1729
- where: q.where.map((t, i) => i === index ? clause : t)
1730
- })),
1731
- [setQuery]
1732
- );
1733
- const removeFilter = useCallback8(
1734
- (index) => setQuery((q) => ({
1735
- ...q,
1736
- offset: 0,
1737
- where: q.where.filter((_, i) => i !== index)
1738
- })),
1739
- [setQuery]
1740
- );
1741
- const clearFilters = useCallback8(
1742
- () => setQuery((q) => ({ ...q, offset: 0, where: [] })),
1743
- [setQuery]
1744
- );
1745
- const updatePredicate = useCallback8(
1746
- (termIndex, predIndex, clause) => setQuery((q) => ({
1747
- ...q,
1748
- offset: 0,
1749
- where: q.where.map((term, i) => {
1750
- if (i !== termIndex) return term;
1751
- if (isOrGroup(term))
1752
- return {
1753
- any: term.any.map((c, j) => j === predIndex ? clause : c)
1754
- };
1755
- return clause;
1756
- })
1757
- })),
1758
- [setQuery]
1759
- );
1760
- const removePredicate = useCallback8(
1761
- (termIndex, predIndex) => setQuery((q) => {
1762
- const where = [];
1763
- q.where.forEach((term, i) => {
1764
- if (i !== termIndex) {
1765
- where.push(term);
1766
- return;
1767
- }
1768
- if (isOrGroup(term)) {
1769
- const members = term.any.filter((_, j) => j !== predIndex);
1770
- if (members.length === 1) where.push(members[0]);
1771
- else if (members.length > 1) where.push({ any: members });
1772
- return;
1773
- }
1774
- });
1775
- return { ...q, offset: 0, where };
1776
- }),
1777
- [setQuery]
1778
- );
1779
- const negateOne = useCallback8(
1780
- (clause) => {
1781
- const field = byName.get(clause.field);
1782
- return negateClause(clause, field ? opsForField(field) : void 0);
2857
+ const { addFilter, updateFilter, removeFilter, clearFilters, updatePredicate, removePredicate, negatePredicate, reorderFilters, mergeFilters } = useQueryFilters(schema, setQuery);
2858
+ const resetAll = useCallback12(
2859
+ () => {
2860
+ const baseline = savedBaseline.current;
2861
+ viewRevision.current++;
2862
+ setQuery(cloneQueryState(baseline?.source === sourceSchema.name ? baseline.query : defaults));
1783
2863
  },
1784
- [byName]
2864
+ [setQuery, defaults, sourceSchema.name]
1785
2865
  );
1786
- const negatePredicate = useCallback8(
1787
- (termIndex, predIndex) => setQuery((q) => ({
1788
- ...q,
1789
- offset: 0,
1790
- where: q.where.map((term, i) => {
1791
- if (i !== termIndex) return term;
1792
- if (isOrGroup(term))
1793
- return {
1794
- any: term.any.map((c, j) => j === predIndex ? negateOne(c) : c)
1795
- };
1796
- return negateOne(term);
1797
- })
1798
- })),
1799
- [setQuery, negateOne]
1800
- );
1801
- const reorderFilters = useCallback8(
1802
- (fromIndex, toIndex) => setQuery((q) => {
1803
- if (fromIndex < 0 || fromIndex >= q.where.length) return q;
1804
- const where = [...q.where];
1805
- const [moved] = where.splice(fromIndex, 1);
1806
- where.splice(Math.max(0, Math.min(toIndex, where.length)), 0, moved);
1807
- return { ...q, offset: 0, where };
1808
- }),
1809
- [setQuery]
1810
- );
1811
- const mergeFilters = useCallback8(
1812
- (sourceIndex, targetIndex) => setQuery((q) => {
1813
- if (sourceIndex === targetIndex) return q;
1814
- const source = q.where[sourceIndex];
1815
- const target = q.where[targetIndex];
1816
- if (!source || !target) return q;
1817
- const merged = {
1818
- any: [...predicatesOf(target), ...predicatesOf(source)]
1819
- };
1820
- const where = [];
1821
- q.where.forEach((term, i) => {
1822
- if (i === sourceIndex) return;
1823
- where.push(i === targetIndex ? merged : term);
1824
- });
1825
- return { ...q, offset: 0, where };
1826
- }),
1827
- [setQuery]
1828
- );
1829
- const resetAll = useCallback8(
1830
- () => setQuery(() => cloneQueryState(defaults)),
1831
- [setQuery, defaults]
1832
- );
1833
- const addAggregation = useCallback8(
2866
+ const addAggregation = useCallback12(
1834
2867
  (partial) => {
1835
2868
  const id = `a${nowRef.current()}-${aggIdRef.current++}`;
1836
2869
  const clause = {
@@ -1847,7 +2880,7 @@ function useQueryTable(opts) {
1847
2880
  },
1848
2881
  [setQuery]
1849
2882
  );
1850
- const updateAggregation = useCallback8(
2883
+ const updateAggregation = useCallback12(
1851
2884
  (id, patch2) => setQuery((q) => ({
1852
2885
  ...q,
1853
2886
  aggregations: (q.aggregations ?? []).map(
@@ -1856,7 +2889,7 @@ function useQueryTable(opts) {
1856
2889
  })),
1857
2890
  [setQuery]
1858
2891
  );
1859
- const moveAggregation = useCallback8(
2892
+ const moveAggregation = useCallback12(
1860
2893
  (id, toIndex) => setQuery((q) => {
1861
2894
  const current = q.aggregations ?? [];
1862
2895
  const from = current.findIndex((a) => a.id === id);
@@ -1868,22 +2901,22 @@ function useQueryTable(opts) {
1868
2901
  }),
1869
2902
  [setQuery]
1870
2903
  );
1871
- const removeAggregation = useCallback8(
2904
+ const removeAggregation = useCallback12(
1872
2905
  (id) => setQuery((q) => ({
1873
2906
  ...q,
1874
2907
  aggregations: (q.aggregations ?? []).filter((a) => a.id !== id)
1875
2908
  })),
1876
2909
  [setQuery]
1877
2910
  );
1878
- const clearAggregations = useCallback8(
2911
+ const clearAggregations = useCallback12(
1879
2912
  () => setQuery((q) => ({ ...q, aggregations: [] })),
1880
2913
  [setQuery]
1881
2914
  );
1882
- const setSort = useCallback8(
2915
+ const setSort = useCallback12(
1883
2916
  (orderBy) => setQuery((q) => ({ ...q, offset: 0, orderBy })),
1884
2917
  [setQuery]
1885
2918
  );
1886
- const toggleSort = useCallback8(
2919
+ const toggleSort = useCallback12(
1887
2920
  (field, additive = false) => setQuery((q) => ({
1888
2921
  ...q,
1889
2922
  offset: 0,
@@ -1891,22 +2924,22 @@ function useQueryTable(opts) {
1891
2924
  })),
1892
2925
  [setQuery]
1893
2926
  );
1894
- const setLimit = useCallback8(
2927
+ const setLimit = useCallback12(
1895
2928
  (limit) => patch({ limit, offset: 0 }),
1896
2929
  [patch]
1897
2930
  );
1898
- const setOffset = useCallback8((offset) => patch({ offset }), [patch]);
1899
- const nextPage = useCallback8(
2931
+ const setOffset = useCallback12((offset) => patch({ offset }), [patch]);
2932
+ const nextPage = useCallback12(
1900
2933
  () => setQuery((q) => ({ ...q, offset: q.offset + q.limit })),
1901
2934
  [setQuery]
1902
2935
  );
1903
- const prevPage = useCallback8(
2936
+ const prevPage = useCallback12(
1904
2937
  () => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })),
1905
2938
  [setQuery]
1906
2939
  );
1907
- const filterValues = useCallback8(
2940
+ const filterValues = useCallback12(
1908
2941
  async (field, search) => {
1909
- if (!transport?.fetchDistinctValues) {
2942
+ if (!metadataTransport?.fetchDistinctValues || query.grouping && !query.grouping.keys.includes(field)) {
1910
2943
  const f = byName.get(field);
1911
2944
  if (!f || !clientRows) return { values: [], hasMore: false };
1912
2945
  const fieldHasNull = fieldHasNullInRows(f, clientRows);
@@ -1917,18 +2950,18 @@ function useQueryTable(opts) {
1917
2950
  hasNull: fieldHasNull
1918
2951
  };
1919
2952
  }
1920
- return transport.fetchDistinctValues({ field, search });
2953
+ return metadataTransport.fetchDistinctValues({ field, search });
1921
2954
  },
1922
- [transport, byName, clientRows]
2955
+ [metadataTransport, byName, clientRows, groupKey]
1923
2956
  );
1924
- const fieldStats = useCallback8(
2957
+ const fieldStats = useCallback12(
1925
2958
  async (fields, signal) => {
1926
2959
  const requested = [...new Set(fields)];
1927
- if (transport?.fetchFieldStats) {
2960
+ if (metadataTransport?.fetchFieldStats && !query.grouping) {
1928
2961
  const backendFields = requested.filter(
1929
2962
  (name) => byName.get(name)?.source.kind === "backend"
1930
2963
  );
1931
- return backendFields.length ? transport.fetchFieldStats(backendFields, signal) : {};
2964
+ return backendFields.length ? metadataTransport.fetchFieldStats(backendFields, signal) : {};
1932
2965
  }
1933
2966
  if (!clientRows) return {};
1934
2967
  const result = {};
@@ -1945,14 +2978,14 @@ function useQueryTable(opts) {
1945
2978
  }
1946
2979
  return result;
1947
2980
  },
1948
- [transport, byName, clientRows]
2981
+ [metadataTransport, byName, clientRows, groupKey]
1949
2982
  );
1950
- const refresh = useCallback8(() => {
2983
+ const refresh = useCallback12(() => {
1951
2984
  setNonce((n) => n + 1);
1952
2985
  onRefreshRef.current?.();
1953
2986
  }, []);
1954
- const stopAutoRefresh = useCallback8(() => setAutoRefreshStatus(null), []);
1955
- const startAutoRefresh = useCallback8(
2987
+ const stopAutoRefresh = useCallback12(() => setAutoRefreshStatus(null), []);
2988
+ const startAutoRefresh = useCallback12(
1956
2989
  ({ frequencyMs, turnOffAfterMs }) => {
1957
2990
  const pollCount = frequencyMs > 0 && turnOffAfterMs > 0 ? Math.floor(turnOffAfterMs / frequencyMs) : 0;
1958
2991
  if (pollCount < 1 || pollCount > MAX_AUTO_REFRESH_POLLS) {
@@ -1972,7 +3005,7 @@ function useQueryTable(opts) {
1972
3005
  );
1973
3006
  const autoRefreshFrequencyMs = autoRefreshStatus?.frequencyMs ?? null;
1974
3007
  const autoRefreshStopsAt = autoRefreshStatus?.stopsAt ?? null;
1975
- useEffect5(() => {
3008
+ useEffect10(() => {
1976
3009
  if (autoRefreshFrequencyMs == null || autoRefreshStopsAt == null) return;
1977
3010
  const tick = () => {
1978
3011
  if (nowRef.current() >= autoRefreshStopsAt) {
@@ -2000,7 +3033,7 @@ function useQueryTable(opts) {
2000
3033
  clearTimeout(timeout);
2001
3034
  };
2002
3035
  }, [autoRefreshFrequencyMs, autoRefreshStopsAt, refresh]);
2003
- const autoRefresh = useMemo7(
3036
+ const autoRefresh = useMemo9(
2004
3037
  () => ({
2005
3038
  status: autoRefreshStatus,
2006
3039
  start: startAutoRefresh,
@@ -2008,7 +3041,7 @@ function useQueryTable(opts) {
2008
3041
  }),
2009
3042
  [autoRefreshStatus, startAutoRefresh, stopAutoRefresh]
2010
3043
  );
2011
- const refreshRow = useCallback8(
3044
+ const refreshRow = useCallback12(
2012
3045
  async (id) => {
2013
3046
  if (opts.computedExecution && transport?.fetchRowsV2) {
2014
3047
  refresh();
@@ -2022,11 +3055,11 @@ function useQueryTable(opts) {
2022
3055
  },
2023
3056
  [transport, rowId, opts.computedExecution, refresh]
2024
3057
  );
2025
- const visibleFields = useMemo7(
2026
- () => selectedFields2(displaySchema, query),
2027
- [displaySchema, query]
3058
+ const visibleFields = useMemo9(
3059
+ () => selectedFields2(columnSchema, query),
3060
+ [columnSchema, query]
2028
3061
  );
2029
- const aggregations = useMemo7(
3062
+ const aggregations = useMemo9(
2030
3063
  () => ({
2031
3064
  clauses: query.aggregations ?? [],
2032
3065
  results: aggState.results,
@@ -2061,14 +3094,42 @@ function useQueryTable(opts) {
2061
3094
  query.where
2062
3095
  ]
2063
3096
  );
2064
- return {
3097
+ const sourceRowId = useCallback12((row) => {
3098
+ const field = sourceSchema.fields.find((f) => f.name === sourceSchema.idField);
3099
+ const value = field ? readFieldValue4(field, row) : row[sourceSchema.idField];
3100
+ return typeof value === "string" || typeof value === "number" && Number.isFinite(value) ? value : null;
3101
+ }, [sourceSchema]);
3102
+ const itemDetails = useItemDetails({
3103
+ schema: query.grouping ? sourceSchema : displaySchema,
3104
+ query: sourceQuery,
3105
+ setQuery: setSourceQuery,
3106
+ options: opts.itemDetails,
3107
+ rows: query.grouping ? sourceRows ?? [] : rows,
3108
+ clientRows: sourceRows,
3109
+ rowId: sourceRowId,
3110
+ transport: metadataTransport,
3111
+ refreshKey: nonce,
3112
+ links,
3113
+ queryParameter: parameter,
3114
+ queryReady: storageReady && !restoration.loading && !restoration.error
3115
+ });
3116
+ const api = {
3117
+ restoration: {
3118
+ status: restoration.error ? "error" : !storageReady || restoration.loading ? "resolving" : "ready",
3119
+ ready: active && storageReady && !restoration.loading && !restoration.error,
3120
+ error: restoration.error
3121
+ },
3122
+ restoreLocation,
3123
+ links,
3124
+ schema: columnSchema,
3125
+ ...itemDetails && detailsEnabled ? { itemDetails } : {},
2065
3126
  query,
2066
- defaults,
3127
+ defaults: resultDefaults,
2067
3128
  setQuery,
2068
3129
  rows,
2069
3130
  total,
2070
- loading,
2071
- error,
3131
+ loading: loading || restoration.loading,
3132
+ error: restoration.error ?? error,
2072
3133
  requestActivity,
2073
3134
  canUndo,
2074
3135
  canRedo,
@@ -2104,6 +3165,48 @@ function useQueryTable(opts) {
2104
3165
  visibleFields,
2105
3166
  rowId
2106
3167
  };
3168
+ const sourceApi = {
3169
+ ...sourceFilters,
3170
+ loading,
3171
+ rowId: sourceRowId,
3172
+ query: sourceQuery,
3173
+ setQuery: setSourceQuery,
3174
+ ...itemDetails ? { itemDetails } : {},
3175
+ schema: sourceCatalogue,
3176
+ filterValues: async (name, search) => {
3177
+ if (metadataTransport?.fetchDistinctValues) return metadataTransport.fetchDistinctValues({ field: name, search });
3178
+ const field = groupingModel.sourceField(name);
3179
+ return field && sourceRows ? { ...distinctValuesFromRows(field, sourceRows, search), hasNull: fieldHasNullInRows(field, sourceRows) } : { values: [], hasMore: false };
3180
+ },
3181
+ fieldStats: async (names, signal) => {
3182
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
3183
+ const requested = [...new Set(names)];
3184
+ if (metadataTransport?.fetchFieldStats) {
3185
+ const backendFields = requested.filter(
3186
+ (name) => sourceCatalogue.fields.find((field) => field.name === name)?.source.kind === "backend"
3187
+ );
3188
+ return backendFields.length ? metadataTransport.fetchFieldStats(backendFields, signal) : {};
3189
+ }
3190
+ return Object.fromEntries(requested.flatMap((name) => {
3191
+ const field = groupingModel.sourceField(name);
3192
+ return field && sourceRows ? [[name, { distinct: new Set(sourceRows.map((row) => JSON.stringify(readFieldValue4(field, row)))).size }]] : [];
3193
+ }));
3194
+ }
3195
+ };
3196
+ const grouping = useGroupingController(
3197
+ api,
3198
+ sourceApi,
3199
+ groupingModel,
3200
+ prepared?.inputs ?? sourceRows ?? [],
3201
+ groupingSupported,
3202
+ viewRevision.current,
3203
+ { inputsReady: !remoteGrouping, ...remoteGrouping ? { preview: previewGrouping } : {} }
3204
+ );
3205
+ return {
3206
+ ...api,
3207
+ ...groupingSupported || query.grouping ? { grouping } : {},
3208
+ select: { ...select, hide: (field) => query.grouping?.keys.includes(field) ? grouping.changeKeys(query.grouping.keys.filter((key) => key !== field)) : select.hide(field) }
3209
+ };
2107
3210
  }
2108
3211
  function nextOrderBy(existing, field, additive) {
2109
3212
  const i = existing.findIndex((s) => s.field === field);
@@ -2123,11 +3226,67 @@ function nextOrderBy(existing, field, additive) {
2123
3226
  }
2124
3227
  return existing.filter((_, k) => k !== i);
2125
3228
  }
3229
+
3230
+ // src/useObjectReferences.ts
3231
+ import { useEffect as useEffect11, useMemo as useMemo10, useState as useState12 } from "react";
3232
+ import {
3233
+ objectReferenceKey
3234
+ } from "@pythia-software/query-table-core";
3235
+ function useObjectReferences(references, adapter) {
3236
+ const fingerprint = JSON.stringify(references.map(objectReferenceKey).sort());
3237
+ const batch = useMemo10(
3238
+ () => [
3239
+ ...new Map(
3240
+ references.map((reference) => [
3241
+ objectReferenceKey(reference),
3242
+ reference
3243
+ ])
3244
+ ).values()
3245
+ ],
3246
+ [fingerprint]
3247
+ );
3248
+ const [state, setState] = useState12({
3249
+ fingerprint: "",
3250
+ adapter: void 0,
3251
+ values: /* @__PURE__ */ new Map(),
3252
+ error: null
3253
+ });
3254
+ useEffect11(() => {
3255
+ const controller = new AbortController();
3256
+ if (!adapter || batch.length === 0) return;
3257
+ const allowed = new Set(batch.map(objectReferenceKey));
3258
+ void Promise.resolve().then(() => adapter.resolve(batch, controller.signal)).then((results) => {
3259
+ if (controller.signal.aborted) return;
3260
+ const values = /* @__PURE__ */ new Map();
3261
+ for (const result of results) {
3262
+ const key = objectReferenceKey(result.reference);
3263
+ if (allowed.has(key)) values.set(key, result);
3264
+ }
3265
+ setState({ fingerprint, adapter, values, error: null });
3266
+ }).catch((error) => {
3267
+ if (!controller.signal.aborted)
3268
+ setState({
3269
+ fingerprint,
3270
+ adapter,
3271
+ values: /* @__PURE__ */ new Map(),
3272
+ error: error instanceof Error ? error : new Error(String(error))
3273
+ });
3274
+ });
3275
+ return () => controller.abort();
3276
+ }, [batch, adapter]);
3277
+ const current = state.fingerprint === fingerprint && state.adapter === adapter;
3278
+ return {
3279
+ values: current ? state.values : /* @__PURE__ */ new Map(),
3280
+ error: current ? state.error : null
3281
+ };
3282
+ }
2126
3283
  export {
2127
3284
  createFormulaWorker,
2128
3285
  evaluateFormulaRows,
2129
3286
  useAggregations,
2130
3287
  useColumnDrag,
3288
+ useObjectReferences,
3289
+ usePropertyView,
2131
3290
  useQueryTable,
2132
3291
  useSavedQueries,
2133
3292
  useSelect,