@pythia-software/query-table-react 0.9.6 → 0.9.7

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,11 +1,28 @@
1
1
  // src/useQueryFilters.ts
2
2
  import { useCallback, useMemo } from "react";
3
3
  import {
4
+ branchesOf,
4
5
  isOrGroup,
5
- predicatesOf,
6
+ isWhereConjunction,
6
7
  negateClause,
7
8
  opsForField
8
9
  } from "@pythia-software/query-table-core";
10
+ function editBranchPredicate(term, predIndex, edit) {
11
+ let k = 0;
12
+ const any = [];
13
+ for (const member of term.any) {
14
+ const kept = [];
15
+ for (const clause of isWhereConjunction(member) ? member.all : [member]) {
16
+ const next = k++ === predIndex ? edit(clause) : clause;
17
+ if (next) kept.push(next);
18
+ }
19
+ if (kept.length === 1) any.push(kept[0]);
20
+ else if (kept.length > 1) any.push({ all: kept });
21
+ }
22
+ if (any.length > 1) return [{ any }];
23
+ const only = any[0];
24
+ return !only ? [] : isWhereConjunction(only) ? only.all : [only];
25
+ }
9
26
  function useQueryFilters(schema, setQuery) {
10
27
  const byName = useMemo(
11
28
  () => new Map(schema.fields.map((field) => [field.name, field])),
@@ -45,8 +62,9 @@ function useQueryFilters(schema, setQuery) {
45
62
  return {
46
63
  any: term.any.map((c, j) => j === predIndex ? clause : c)
47
64
  };
65
+ if ("any" in term) return editBranchPredicate(term, predIndex, () => clause);
48
66
  return clause;
49
- })
67
+ }).flat()
50
68
  })),
51
69
  [setQuery]
52
70
  );
@@ -64,6 +82,10 @@ function useQueryFilters(schema, setQuery) {
64
82
  else if (members.length > 1) where.push({ any: members });
65
83
  return;
66
84
  }
85
+ if ("any" in term) {
86
+ where.push(...editBranchPredicate(term, predIndex, () => null));
87
+ return;
88
+ }
67
89
  });
68
90
  return { ...q, offset: 0, where };
69
91
  }),
@@ -86,8 +108,9 @@ function useQueryFilters(schema, setQuery) {
86
108
  return {
87
109
  any: term.any.map((c, j) => j === predIndex ? negateOne(c) : c)
88
110
  };
111
+ if ("any" in term) return editBranchPredicate(term, predIndex, negateOne);
89
112
  return negateOne(term);
90
- })
113
+ }).flat()
91
114
  })),
92
115
  [setQuery, negateOne]
93
116
  );
@@ -108,7 +131,9 @@ function useQueryFilters(schema, setQuery) {
108
131
  const target = q.where[targetIndex];
109
132
  if (!source || !target) return q;
110
133
  const merged = {
111
- any: [...predicatesOf(target), ...predicatesOf(source)]
134
+ any: [...branchesOf(target), ...branchesOf(source)].map(
135
+ (branch) => branch.length === 1 ? branch[0] : { all: branch }
136
+ )
112
137
  };
113
138
  const where = [];
114
139
  q.where.forEach((term, i) => {
@@ -135,9 +160,10 @@ function useQueryFilters(schema, setQuery) {
135
160
  // src/useGrouping.ts
136
161
  import { useEffect, useRef, useState } from "react";
137
162
  import {
163
+ groupKeyFields,
138
164
  MAX_SUMMARY_COLUMNS,
139
165
  normalizeQueryState,
140
- predicatesOf as predicatesOf2,
166
+ predicatesOf,
141
167
  withGroupingKeys,
142
168
  withoutGrouping,
143
169
  queriesEqual
@@ -150,7 +176,8 @@ function useGroupingController(api, source, model, inputs, supported, viewRevisi
150
176
  const view = {
151
177
  version: 2,
152
178
  mode: stage ? "groups" : "rows",
153
- keys: stage?.keys ?? [],
179
+ keys: groupKeyFields(stage?.keys ?? []),
180
+ groupKeys: stage?.keys ?? [],
154
181
  summaries: stage?.summaries ?? [],
155
182
  representative: stage?.representative ?? null,
156
183
  input: source.query,
@@ -205,7 +232,7 @@ function useGroupingController(api, source, model, inputs, supported, viewRevisi
205
232
  offset: 0,
206
233
  aggregations: [],
207
234
  where: q.where.filter(
208
- (term) => !predicatesOf2(term).some((p) => changed.has(p.field))
235
+ (term) => !predicatesOf(term).some((p) => changed.has(p.field))
209
236
  ),
210
237
  orderBy: q.orderBy.filter((term) => !changed.has(term.field))
211
238
  } : {}
@@ -213,7 +240,7 @@ function useGroupingController(api, source, model, inputs, supported, viewRevisi
213
240
  );
214
241
  setNotice(
215
242
  changed.size && (api.query.where.some(
216
- (term) => predicatesOf2(term).some((p) => changed.has(p.field))
243
+ (term) => predicatesOf(term).some((p) => changed.has(p.field))
217
244
  ) || api.query.orderBy.some((term) => changed.has(term.field)) || api.query.aggregations?.length) ? "Columns updated. Affected filters, sorts, and metrics cleared; Undo restores them." : ""
218
245
  );
219
246
  }
@@ -509,7 +536,7 @@ function initialDetailModes(config) {
509
536
  const available = ALL_MODES.filter((mode) => !config?.modes || config.modes.includes(mode));
510
537
  return config?.defaultModes ? available.filter((mode) => config.defaultModes.includes(mode)) : available.includes("modal") ? ["modal"] : available.slice(0, 1);
511
538
  }
512
- function useDetailProperties(mode, schema, config, query, setQuery, enabled) {
539
+ function useDetailProperties(mode, schema, config, query, setQuery, enabled, readOnly = false) {
513
540
  const snapshot = query.itemDetails?.views?.[mode] ?? query.itemDetails?.view;
514
541
  const seed = useRef3(snapshot);
515
542
  const properties = usePropertyView({
@@ -539,7 +566,7 @@ function useDetailProperties(mode, schema, config, query, setQuery, enabled) {
539
566
  if (target !== localViewKey) properties.setView(next);
540
567
  } else if (pendingView.current && pendingView.current.query === queryViewKey) {
541
568
  if (pendingView.current.local === localViewKey) pendingView.current = null;
542
- } else if (!properties.loading && localViewKey !== queryViewKey && (queryViewKey !== void 0 || localViewKey !== previous.local)) {
569
+ } else if (!readOnly && !properties.loading && localViewKey !== queryViewKey && (queryViewKey !== void 0 || localViewKey !== previous.local)) {
543
570
  setQuery((current) => ({ ...current, itemDetails: {
544
571
  modes: current.itemDetails?.modes ?? initialDetailModes(config),
545
572
  ...current.itemDetails,
@@ -561,7 +588,9 @@ function useItemDetails({
561
588
  refreshKey,
562
589
  links,
563
590
  queryReady = true,
564
- queryParameter: queryParameter2
591
+ queryParameter: queryParameter2,
592
+ lookupRow,
593
+ subject
565
594
  }) {
566
595
  const enabled = Boolean(options);
567
596
  const config = typeof options === "object" ? options : void 0;
@@ -573,9 +602,10 @@ function useItemDetails({
573
602
  const sharedModal = requested?.mode === "modal" && query.itemDetails?.modes.includes("modal") && (availableModes.includes("hover") || availableModes.includes("page") && !page.urlTemplate.trim());
574
603
  const active = requested && (modes.includes(requested.mode) || sharedModal) ? requested : null;
575
604
  const syncProperties = enabled && queryReady;
576
- const modalProperties = useDetailProperties("modal", detailSchema, config, query, setQuery, syncProperties);
577
- const hoverProperties = useDetailProperties("hover", detailSchema, config, query, setQuery, syncProperties);
578
- const expandProperties = useDetailProperties("expand", detailSchema, config, query, setQuery, syncProperties);
605
+ const readOnly = subject === "group";
606
+ const modalProperties = useDetailProperties("modal", detailSchema, config, query, setQuery, syncProperties, readOnly);
607
+ const hoverProperties = useDetailProperties("hover", detailSchema, config, query, setQuery, syncProperties, readOnly);
608
+ const expandProperties = useDetailProperties("expand", detailSchema, config, query, setQuery, syncProperties, readOnly);
579
609
  const propertiesByMode = { modal: modalProperties, hover: hoverProperties, expand: expandProperties };
580
610
  const views = {
581
611
  modal: modalProperties.view,
@@ -585,10 +615,14 @@ function useItemDetails({
585
615
  const customLoad = config?.loadRow;
586
616
  const customLoadRef = useRef3(customLoad);
587
617
  customLoadRef.current = customLoad;
618
+ const lookupRef = useRef3(lookupRow);
619
+ lookupRef.current = lookupRow;
620
+ const hasLookup = Boolean(lookupRow);
588
621
  const hasCustomLoad = Boolean(customLoad);
589
622
  const sourceType = config?.objectType ?? detailSchema.name;
590
623
  const fallbackRows = !customLoad && !clientRows && !transport?.fetchRow ? rows : void 0;
591
624
  const loadRow = useCallback3(async (id, signal) => {
625
+ if (lookupRef.current) return lookupRef.current(id) ?? null;
592
626
  if (customLoadRef.current) return customLoadRef.current(id, signal);
593
627
  if (clientRows) {
594
628
  const row2 = clientRows.find((row3) => rowId(row3) === id);
@@ -600,10 +634,10 @@ function useItemDetails({
600
634
  }
601
635
  const row = rows.find((row2) => rowId(row2) === id);
602
636
  return row ? { row, loadedFields: Object.keys(row) } : null;
603
- }, [hasCustomLoad, sourceType, clientRows, transport?.fetchRow, fallbackRows, rowId]);
637
+ }, [hasLookup, hasCustomLoad, sourceType, clientRows, transport?.fetchRow, fallbackRows, rowId]);
604
638
  const [retryKey, setRetryKey] = useState3(0);
605
639
  const [result, setResult] = useState3({ source: void 0, epoch: "", id: void 0, record: null, loading: false, error: null });
606
- const activeId = enabled ? active?.id : void 0;
640
+ const activeId = enabled && !lookupRow ? active?.id : void 0;
607
641
  const epoch = `${refreshKey}:${retryKey}`;
608
642
  const currentResult = result.id === activeId && result.source === loadRow && result.epoch === epoch;
609
643
  useEffect3(() => {
@@ -628,7 +662,9 @@ function useItemDetails({
628
662
  return () => controller.abort();
629
663
  }, [activeId, loadRow, refreshKey, retryKey]);
630
664
  if (!enabled) return;
665
+ const looked = lookupRow && active ? lookupRow(active.id) : null;
631
666
  return {
667
+ ...subject ? { subject } : {},
632
668
  ...links ? { links } : {},
633
669
  schema: detailSchema,
634
670
  queryParameter: queryParameter2 ?? config?.queryParameter ?? "q",
@@ -639,9 +675,9 @@ function useItemDetails({
639
675
  active,
640
676
  page,
641
677
  pageSchema: schema,
642
- record: currentResult ? result.record : null,
643
- loading: activeId !== void 0 && (!currentResult || result.loading),
644
- error: currentResult ? result.error : null,
678
+ record: lookupRow ? looked ?? null : currentResult ? result.record : null,
679
+ loading: lookupRow ? looked === void 0 : activeId !== void 0 && (!currentResult || result.loading),
680
+ error: lookupRow ? null : currentResult ? result.error : null,
645
681
  ...config?.relationships ? { relationships: config.relationships } : {},
646
682
  loadRow,
647
683
  retry: () => setRetryKey((value) => value + 1),
@@ -674,6 +710,7 @@ function useItemDetails({
674
710
 
675
711
  // src/computedExecution.ts
676
712
  import {
713
+ predicatesOf as predicatesOf2,
677
714
  isComputedField,
678
715
  readFieldValue
679
716
  } from "@pythia-software/query-table-core";
@@ -689,7 +726,7 @@ function validateComputedResponse(request, result, schema) {
689
726
  const executed = /* @__PURE__ */ new Set([
690
727
  ...request.select.filter(isComputedField),
691
728
  ...request.orderBy.map((term) => term.field).filter(isComputedField),
692
- ...request.where.flatMap((term) => ("any" in term ? term.any : [term]).map((predicate) => predicate.field)).filter(isComputedField)
729
+ ...request.where.flatMap((term) => predicatesOf2(term).map((predicate) => predicate.field)).filter(isComputedField)
693
730
  ]);
694
731
  for (const [name, revision] of Object.entries(request.expectedRevisions)) {
695
732
  const id = name.replace(/^@computed\//, "");
@@ -724,6 +761,7 @@ function validateComputedResponse(request, result, schema) {
724
761
 
725
762
  // src/previewRows.ts
726
763
  import {
764
+ predicatesOf as predicatesOf3,
727
765
  sourceExecutionSchema,
728
766
  toQueryRequestV3,
729
767
  toServerQuery,
@@ -734,13 +772,13 @@ async function fetchPreviewRows(transport, query, schema, execution, definitions
734
772
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
735
773
  const sourceSchema = sourceExecutionSchema(baseSchema, execution, definitions);
736
774
  let result;
737
- const computedFilter = query.where.some((term) => ("any" in term ? term.any : [term]).some((predicate) => predicate.field.startsWith("@computed/")));
775
+ const computedFilter = query.where.some((term) => predicatesOf3(term).some((predicate) => predicate.field.startsWith("@computed/")));
738
776
  if (transport.fetchRowsV2 && execution && !query.grouping && !computedFilter) {
739
777
  const request = toServerQueryV2(query, sourceSchema, execution);
740
778
  if (request.diagnostics.length)
741
779
  throw Error(request.diagnostics.map((issue) => issue.message).join(" "));
742
780
  for (const definition of definitions) {
743
- if (executionRevision(execution, definition.id) !== definition.revision && (request.select.includes(`@computed/${definition.id}`) || request.orderBy.some((term) => term.field === `@computed/${definition.id}`) || request.where.some((term) => ("any" in term ? term.any : [term]).some((predicate) => predicate.field === `@computed/${definition.id}`))))
781
+ if (executionRevision(execution, definition.id) !== definition.revision && (request.select.includes(`@computed/${definition.id}`) || request.orderBy.some((term) => term.field === `@computed/${definition.id}`) || request.where.some((term) => predicatesOf3(term).some((predicate) => predicate.field === `@computed/${definition.id}`))))
744
782
  throw Error("Computed definitions changed. Refresh the server execution handshake.");
745
783
  }
746
784
  const response = await transport.fetchRowsV2(request, signal);
@@ -772,6 +810,7 @@ async function fetchPreviewRows(transport, query, schema, execution, definitions
772
810
  // src/useComputedColumns.ts
773
811
  import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
774
812
  import {
813
+ predicatesOf as predicatesOf4,
775
814
  isGroupLinkField,
776
815
  applyQuery,
777
816
  compileFormula,
@@ -1173,7 +1212,7 @@ function useComputedColumns(options) {
1173
1212
  offset: 0
1174
1213
  };
1175
1214
  const now = options.now ?? Date.now();
1176
- const computedFilter = request.where.some((term) => ("any" in term ? term.any : [term]).some((predicate) => isComputedField2(predicate.field)));
1215
+ const computedFilter = request.where.some((term) => predicatesOf4(term).some((predicate) => isComputedField2(predicate.field)));
1177
1216
  const v3 = !(transport.fetchRowsV2 && options.execution && !request.grouping && !computedFilter) && transport.fetchQueryV3 && transport.queryCapabilities?.version === 3;
1178
1217
  const pageSize = Math.min(500, v3 ? transport.queryCapabilities.maxRows : 500);
1179
1218
  if (!Number.isInteger(pageSize) || pageSize < 1) throw Error("Invalid negotiated preview row limit.");
@@ -1262,17 +1301,22 @@ import {
1262
1301
  applyQuery as applyQuery2,
1263
1302
  memoryStorageAdapter,
1264
1303
  normalizeQueryState as normalizeQueryState3,
1304
+ parseHostQueryState,
1265
1305
  readFieldValue as readFieldValue4,
1266
1306
  selectedFields as selectedFields2,
1267
1307
  toServerQuery as toServerQuery2,
1268
1308
  queriesEqual as queriesEqual2,
1269
- isOrGroup as isOrGroup2,
1309
+ isWhereConjunction as isWhereConjunction2,
1310
+ isWhereGroup,
1311
+ predicatesOf as predicatesOf6,
1270
1312
  metricPlans as metricPlans2,
1271
1313
  validateMetric as validateMetric2,
1272
- metricDependencies,
1314
+ metricDependencies as metricDependencies2,
1273
1315
  isComputedField as isComputedField4,
1274
1316
  computedFieldName as computedFieldName2,
1275
- toServerQueryV2 as toServerQueryV22
1317
+ toServerQueryV2 as toServerQueryV22,
1318
+ groupKeyField,
1319
+ groupKeyFields as groupKeyFields2
1276
1320
  } from "@pythia-software/query-table-core";
1277
1321
 
1278
1322
  // src/useRequestActivity.ts
@@ -1385,11 +1429,12 @@ import {
1385
1429
  normalizeQueryState as normalizeQueryState2,
1386
1430
  evaluateMetrics,
1387
1431
  metricPlans,
1432
+ metricDependencies,
1388
1433
  hasRegex as hasRegex2,
1389
1434
  metricPopulationError,
1390
1435
  hasMetricSeries,
1391
1436
  planMetricSeries,
1392
- predicatesOf as predicatesOf3,
1437
+ predicatesOf as predicatesOf5,
1393
1438
  toAggregationQuery,
1394
1439
  toMetricQuery,
1395
1440
  validateMetric,
@@ -1545,11 +1590,10 @@ async function executeMetrics(query, context, signal) {
1545
1590
  shown.flatMap((clause) => [
1546
1591
  ...clause.groupBy,
1547
1592
  ...(clause.where ?? []).flatMap(
1548
- (term) => predicatesOf3(term).map((predicate) => predicate.field)
1593
+ (term) => predicatesOf5(term).map((predicate) => predicate.field)
1549
1594
  ),
1550
- ...metricPlans(clause, context.schema, context.resolveComputed).flatMap(
1551
- (p) => p.dependencies
1552
- )
1595
+ // Includes a measured minimum's field, which the page must also hold.
1596
+ ...metricDependencies(clause, context.schema, context.resolveComputed)
1553
1597
  ])
1554
1598
  );
1555
1599
  for (const name of names) {
@@ -2134,7 +2178,7 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now, snaps
2134
2178
  // src/useQueryLinks.ts
2135
2179
  import { useCallback as useCallback11, useEffect as useEffect9, useRef as useRef8, useState as useState10 } from "react";
2136
2180
  import { encodeQuery } from "@pythia-software/query-table-core";
2137
- function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, onUrlChange) {
2181
+ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, onUrlChange, decorate = (url) => url) {
2138
2182
  const latest = useRef8(query);
2139
2183
  latest.current = query;
2140
2184
  const [kind, setKind] = useState10(null);
@@ -2153,14 +2197,15 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2153
2197
  if (!syncUrl || !ready || typeof window === "undefined") return;
2154
2198
  try {
2155
2199
  const result = service.local(query, window.location.href, parameter);
2156
- window.history.replaceState(window.history.state, "", result.url);
2157
- onUrlChange(result.url);
2200
+ const url = decorate(result.url, query);
2201
+ window.history.replaceState(window.history.state, "", url);
2202
+ onUrlChange(url);
2158
2203
  setKind(result.kind);
2159
2204
  setError(null);
2160
2205
  } catch (e) {
2161
2206
  setError(e instanceof Error ? e : Error(String(e)));
2162
2207
  }
2163
- }, [queryKey, syncUrl, ready, service, parameter, onUrlChange]);
2208
+ }, [queryKey, syncUrl, ready, service, parameter, onUrlChange, decorate]);
2164
2209
  const snapshot = useCallback11(async (q) => {
2165
2210
  const captured = encodeQuery(q);
2166
2211
  const base = typeof window !== "undefined" ? window.location.href : void 0;
@@ -2174,8 +2219,9 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2174
2219
  const url = new URL(base);
2175
2220
  url.searchParams.set(parameter, "@" + saved.code);
2176
2221
  const result = service.local(q, url.toString(), parameter);
2177
- window.history.replaceState(window.history.state, "", result.url);
2178
- onUrlChange(result.url);
2222
+ const next = decorate(result.url, q);
2223
+ window.history.replaceState(window.history.state, "", next);
2224
+ onUrlChange(next);
2179
2225
  setKind("durable");
2180
2226
  }
2181
2227
  return saved;
@@ -2186,7 +2232,7 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2186
2232
  operations.current--;
2187
2233
  if (mounted.current) setBusy(operations.current > 0);
2188
2234
  }
2189
- }, [service, syncUrl, ready, parameter, onUrlChange]);
2235
+ }, [service, syncUrl, ready, parameter, onUrlChange, decorate]);
2190
2236
  const create = useCallback11(async (q = latest.current, options = {}) => {
2191
2237
  const baseUrl = options.baseUrl ?? window.location.href;
2192
2238
  const param = options.parameter ?? parameter;
@@ -2196,7 +2242,8 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2196
2242
  setError(null);
2197
2243
  try {
2198
2244
  if (!ready) throw Error("Resolve or reset the query link before sharing it.");
2199
- const result = await service.durable(q, baseUrl, param);
2245
+ const durable = await service.durable(q, baseUrl, param);
2246
+ const result = param === parameter ? { ...durable, url: decorate(durable.url, q) } : durable;
2200
2247
  if (mounted.current && syncUrl && ready && param === parameter && window.location.href === baseUrl && encodeQuery(latest.current) === captured) {
2201
2248
  window.history.replaceState(window.history.state, "", result.url);
2202
2249
  onUrlChange(result.url);
@@ -2210,8 +2257,11 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2210
2257
  operations.current--;
2211
2258
  if (mounted.current) setBusy(operations.current > 0);
2212
2259
  }
2213
- }, [service, syncUrl, ready, parameter, onUrlChange]);
2214
- const local = useCallback11((q, base = window.location.href, param = parameter) => service.local(q, base, param), [service, parameter]);
2260
+ }, [service, syncUrl, ready, parameter, onUrlChange, decorate]);
2261
+ const local = useCallback11((q, base = window.location.href, param = parameter) => {
2262
+ const result = service.local(q, base, param);
2263
+ return param === parameter ? { ...result, url: decorate(result.url, q) } : result;
2264
+ }, [service, parameter, decorate]);
2215
2265
  return { maxUrlBytes, kind, busy, error, create, local, snapshot };
2216
2266
  }
2217
2267
 
@@ -2248,7 +2298,7 @@ function defaultsFor(schema, details = false) {
2248
2298
  });
2249
2299
  }
2250
2300
  function cloneWhereTerm(term) {
2251
- const copy = isOrGroup2(term) ? { ...term, any: term.any.map((c) => ({ ...c })) } : { ...term };
2301
+ const copy = isWhereGroup(term) ? { ...term, any: term.any.map((c) => isWhereConjunction2(c) ? { all: c.all.map((m) => ({ ...m })) } : { ...c }) } : { ...term };
2252
2302
  if (term.setFilter)
2253
2303
  copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
2254
2304
  return copy;
@@ -2264,6 +2314,7 @@ function cloneQueryState(q) {
2264
2314
  offset: q.offset
2265
2315
  };
2266
2316
  if (q.fitToScreen) clone.fitToScreen = true;
2317
+ if (q.hostState) clone.hostState = { version: q.hostState.version, params: Object.fromEntries(Object.entries(q.hostState.params).map(([name, values]) => [name, [...values]])) };
2267
2318
  if (q.aggregations)
2268
2319
  clone.aggregations = JSON.parse(
2269
2320
  JSON.stringify(q.aggregations)
@@ -2325,16 +2376,61 @@ function hasQueryLink(url, parameter) {
2325
2376
  function queryParameter(opts) {
2326
2377
  return opts.queryParameter ?? (typeof opts.itemDetails === "object" ? opts.itemDetails.queryParameter : void 0) ?? "q";
2327
2378
  }
2379
+ function withHostState(query, host) {
2380
+ if (!host) {
2381
+ if (query.hostState) throw Error("This query includes host state that this table does not support.");
2382
+ return query;
2383
+ }
2384
+ const raw = query.hostState ?? host.defaults;
2385
+ if (raw.version === 0) throw Error("This query's host state could not be read.");
2386
+ const state = parseHostQueryState(host.normalize ? host.normalize(parseHostQueryState(raw)) : raw);
2387
+ return { ...query, hostState: state };
2388
+ }
2389
+ function withoutHostState(query) {
2390
+ const { hostState: _hostState, ...rest } = query;
2391
+ return rest;
2392
+ }
2393
+ function hostStateFromUrl(location, host) {
2394
+ const url = new URL(location);
2395
+ const params = {};
2396
+ for (const name of host.ownedParameters) {
2397
+ const values = url.searchParams.getAll(name);
2398
+ if (values.length) params[name] = values;
2399
+ }
2400
+ return Object.keys(params).length ? { version: host.defaults.version, params } : void 0;
2401
+ }
2402
+ function writeHostParameters(url, host, state) {
2403
+ const next = new URL(url);
2404
+ for (const name of host.ownedParameters) next.searchParams.delete(name);
2405
+ for (const name of host.ownedParameters)
2406
+ for (const value of state?.params[name] ?? []) next.searchParams.append(name, value);
2407
+ return next.toString();
2408
+ }
2409
+ var HOST_REQUEST_METHODS = /* @__PURE__ */ new Set(["fetchRows", "fetchRowsV2", "fetchQueryV3", "fetchMetrics", "fetchAggregations", "fetchDistinctValues"]);
2410
+ function withHostTransport(transport, hostStateKey) {
2411
+ if (!transport || !hostStateKey) return transport;
2412
+ const hostState = JSON.parse(hostStateKey);
2413
+ const wrappers = /* @__PURE__ */ new Map();
2414
+ return new Proxy(Object.create(transport), {
2415
+ get(_target, key) {
2416
+ const method = Reflect.get(transport, key, transport);
2417
+ if (!HOST_REQUEST_METHODS.has(String(key)) || typeof method !== "function") return method;
2418
+ if (!wrappers.has(key))
2419
+ wrappers.set(key, (request, signal) => method.call(transport, { ...request, hostState }, signal));
2420
+ return wrappers.get(key);
2421
+ }
2422
+ });
2423
+ }
2328
2424
  function resolveInitial(opts) {
2329
- const defaults = defaultsFor(opts.schema, Boolean(opts.itemDetails));
2425
+ const defaults = withHostState(defaultsFor(opts.schema, Boolean(opts.itemDetails)), opts.hostQuery);
2330
2426
  if (opts.initialQuery)
2331
2427
  return {
2332
- q: normalizeQueryState3(opts.initialQuery, defaults),
2428
+ q: withHostState(normalizeQueryState3(opts.initialQuery, defaults), opts.hostQuery),
2333
2429
  fromUrl: false
2334
2430
  };
2335
2431
  if (opts.syncUrl === true && opts.active !== false && typeof window !== "undefined") {
2336
2432
  const parameter = queryParameter(opts);
2337
- if (hasQueryLink(window.location.href, parameter))
2433
+ if (hasQueryLink(window.location.href, parameter) || opts.hostQuery && hostStateFromUrl(window.location.href, opts.hostQuery))
2338
2434
  return {
2339
2435
  q: defaults,
2340
2436
  fromUrl: true
@@ -2353,8 +2449,13 @@ function useQueryTable(opts) {
2353
2449
  const active = opts.active !== false;
2354
2450
  const urlSyncEnabled = opts.syncUrl === true;
2355
2451
  const syncUrl = urlSyncEnabled && active;
2356
- const { transport: metadataTransport, metricTransport, activity: requestActivity } = useRequestActivity(sourceTransport, opts.metricTransport);
2357
- const transport = metadataTransport;
2452
+ const { transport: activityTransport, metricTransport: activityMetricTransport, activity: requestActivity } = useRequestActivity(sourceTransport, opts.metricTransport);
2453
+ const host = opts.hostQuery;
2454
+ const hostRef = useRef9(host);
2455
+ hostRef.current = host;
2456
+ const hostDefaultsKey = host ? JSON.stringify(host.defaults) : "";
2457
+ if (host?.ownedParameters.includes(queryParameter(opts)))
2458
+ throw Error(`hostQuery.ownedParameters cannot include the table's own "${queryParameter(opts)}" parameter.`);
2358
2459
  const storage = useMemo9(
2359
2460
  () => opts.storage ?? memoryStorageAdapter(),
2360
2461
  [opts.storage]
@@ -2367,7 +2468,10 @@ function useQueryTable(opts) {
2367
2468
  const now = opts.now ?? Date.now;
2368
2469
  const nowRef = useRef9(now);
2369
2470
  const onRefreshRef = useRef9(opts.onRefresh);
2370
- const defaults = useMemo9(() => defaultsFor(sourceSchema, Boolean(opts.itemDetails)), [sourceSchema, Boolean(opts.itemDetails)]);
2471
+ const defaults = useMemo9(
2472
+ () => withHostState(defaultsFor(sourceSchema, Boolean(opts.itemDetails)), hostRef.current),
2473
+ [sourceSchema, Boolean(opts.itemDetails), hostDefaultsKey]
2474
+ );
2371
2475
  const initRef = useRef9(resolveInitial(opts));
2372
2476
  const [restoration, setRestoration] = useState11({ loading: initRef.current.fromUrl, error: null });
2373
2477
  const [storageReady, setStorageReady] = useState11(Boolean(initialQuery));
@@ -2404,6 +2508,10 @@ function useQueryTable(opts) {
2404
2508
  () => opts.canonicalizeQuery?.(storedQuery) ?? storedQuery,
2405
2509
  [storedQuery, opts.canonicalizeQuery]
2406
2510
  );
2511
+ const hostStateKey = host && query.hostState ? JSON.stringify(query.hostState) : "";
2512
+ const metadataTransport = useMemo9(() => withHostTransport(activityTransport, hostStateKey), [activityTransport, hostStateKey]);
2513
+ const metricTransport = useMemo9(() => withHostTransport(activityMetricTransport, hostStateKey), [activityMetricTransport, hostStateKey]);
2514
+ const transport = metadataTransport;
2407
2515
  const sourceCatalogue = useMemo9(() => sourceExecutionSchema2(sourceSchema, opts.computedExecution), [sourceSchema, opts.computedExecution]);
2408
2516
  const sourceRows = providedRows;
2409
2517
  const groupingModel = useMemo9(() => createGroupingModel(sourceCatalogue), [sourceCatalogue]);
@@ -2433,6 +2541,7 @@ function useQueryTable(opts) {
2433
2541
  const executionQuery = useMemo9(() => withoutGrouping2(query), [query]);
2434
2542
  const byName = useMemo9(() => new Map(schema.fields.map((field) => [field.name, field])), [schema]);
2435
2543
  const detailsEnabled = Boolean(opts.itemDetails) && !query.grouping;
2544
+ const groupDetailsEnabled = Boolean(opts.itemDetails) && Boolean(query.grouping);
2436
2545
  const validateExecution = useCallback12((q) => {
2437
2546
  if (prepared?.error) throw prepared.error;
2438
2547
  opts.validateQuery?.(query.grouping ? { ...q, grouping: query.grouping } : q);
@@ -2445,6 +2554,7 @@ function useQueryTable(opts) {
2445
2554
  const [loading, setLoading] = useState11(false);
2446
2555
  const [error, setError] = useState11(null);
2447
2556
  const [committedPage, setCommittedPage] = useState11(null);
2557
+ const [failedPage, setFailedPage] = useState11(null);
2448
2558
  const [committedSource, setCommittedSource] = useState11(null);
2449
2559
  const rows = useMemo9(() => committedSource?.schema === schema ? committedRows : [], [committedSource?.schema, schema, committedRows]);
2450
2560
  const [autoRefreshStatus, setAutoRefreshStatus] = useState11(null);
@@ -2464,7 +2574,8 @@ function useQueryTable(opts) {
2464
2574
  updateQueryHistory((history) => {
2465
2575
  const prev = history.query;
2466
2576
  const candidate = typeof next === "function" ? next(prev) : next;
2467
- const nextQuery = normalizeQueryState3(candidate, defaults);
2577
+ const normalized = normalizeQueryState3(candidate, defaults);
2578
+ const nextQuery = withHostState(normalized.hostState || !prev.hostState ? normalized : { ...normalized, hostState: prev.hostState }, hostRef.current);
2468
2579
  if (queriesEqual2(prev, nextQuery)) return history;
2469
2580
  return { query: cloneQueryState(nextQuery), past: [...history.past, cloneQueryState(prev)], future: [] };
2470
2581
  });
@@ -2481,9 +2592,15 @@ function useQueryTable(opts) {
2481
2592
  }, [setQuery]);
2482
2593
  const sourceFilters = useQueryFilters(sourceCatalogue, setSourceQuery);
2483
2594
  const observedQueryUrl = useRef9(null);
2595
+ const tableUrlKey = (url) => {
2596
+ const params = new URL(url).searchParams;
2597
+ return JSON.stringify([params.getAll(parameter), ...(hostRef.current?.ownedParameters ?? []).map((name) => params.getAll(name))]);
2598
+ };
2484
2599
  const recordQueryUrl = useCallback12((url) => {
2485
- observedQueryUrl.current = JSON.stringify(new URL(url).searchParams.getAll(parameter));
2600
+ observedQueryUrl.current = tableUrlKey(url);
2486
2601
  }, [parameter]);
2602
+ const ownedParametersKey = JSON.stringify(host?.ownedParameters ?? null);
2603
+ const decorateLink = useCallback12((url, q) => hostRef.current ? writeHostParameters(url, hostRef.current, q.hostState) : url, [ownedParametersKey]);
2487
2604
  const links = useQueryLinks(
2488
2605
  query,
2489
2606
  linkService,
@@ -2491,7 +2608,8 @@ function useQueryTable(opts) {
2491
2608
  storageReady && !restoration.loading && !restoration.error,
2492
2609
  parameter,
2493
2610
  opts.links?.maxUrlBytes ?? 2e3,
2494
- recordQueryUrl
2611
+ recordQueryUrl,
2612
+ decorateLink
2495
2613
  );
2496
2614
  const restoreSources = useRef9({ storage, linkService, defaults, initialQuery });
2497
2615
  restoreSources.current = { storage, linkService, defaults, initialQuery };
@@ -2513,17 +2631,21 @@ function useQueryTable(opts) {
2513
2631
  signal?.addEventListener("abort", abort2, { once: true });
2514
2632
  if (signal?.aborted) currentController.abort();
2515
2633
  setStorageReady(false);
2516
- const explicitLink = !!location && (navigation || urlSyncEnabled) && hasQueryLink(location, parameter);
2634
+ const host2 = hostRef.current;
2635
+ const hostFromUrl = host2 && location && (navigation || urlSyncEnabled) ? hostStateFromUrl(location, host2) : void 0;
2636
+ const explicitLink = !!location && (navigation || urlSyncEnabled) && (hasQueryLink(location, parameter) || !!hostFromUrl);
2517
2637
  setRestoration({ loading: true, error: null });
2518
2638
  try {
2519
2639
  if (currentController.signal.aborted) throw new DOMException("Query restoration cancelled.", "AbortError");
2520
2640
  const fromUrl = location && (navigation || urlSyncEnabled) ? await linkService2.resolve(location, parameter, currentController.signal) : null;
2521
- const defaultSaved = fromUrl ? null : await storage2.loadDefaultSaved?.(sourceSchema.name);
2522
- const restored = fromUrl ?? defaultSaved?.query ?? (navigation ? defaults2 : await storage2.loadLast(sourceSchema.name));
2641
+ const linked = fromUrl ?? (hostFromUrl ? defaults2 : null);
2642
+ const defaultSaved = linked ? null : await storage2.loadDefaultSaved?.(sourceSchema.name);
2643
+ let restored = linked ?? defaultSaved?.query ?? (navigation ? defaults2 : await storage2.loadLast(sourceSchema.name));
2523
2644
  if (currentController.signal.aborted) throw new DOMException("Query restoration cancelled.", "AbortError");
2645
+ if (linked && !fromUrl?.hostState && hostFromUrl) restored = { ...linked, hostState: hostFromUrl };
2524
2646
  if (!cancelled && run === generation && epoch === manualQueryEpoch.current) {
2525
2647
  if (restored) {
2526
- const normalized = normalizeQueryState3(restored, defaults2);
2648
+ const normalized = withHostState(normalizeQueryState3(restored, defaults2), host2);
2527
2649
  savedBaseline.current = defaultSaved || fromUrl?.grouping ? { source: sourceSchema.name, query: cloneQueryState(normalized) } : null;
2528
2650
  viewRevision.current++;
2529
2651
  updateQueryHistory(() => ({ query: normalized, past: [], future: [] }));
@@ -2548,7 +2670,7 @@ function useQueryTable(opts) {
2548
2670
  }
2549
2671
  restoreHandler.current = async (location, signal) => {
2550
2672
  location = new URL(location, typeof window !== "undefined" ? window.location.href : void 0).href;
2551
- const key = JSON.stringify(new URL(location).searchParams.getAll(parameter));
2673
+ const key = tableUrlKey(location);
2552
2674
  const current = restorationCurrent.current;
2553
2675
  if (key === observedQueryUrl.current && current.storageReady && !current.restoration.loading && !current.restoration.error) return;
2554
2676
  recordQueryUrl(location);
@@ -2558,7 +2680,7 @@ function useQueryTable(opts) {
2558
2680
  if (typeof window !== "undefined") recordQueryUrl(window.location.href);
2559
2681
  const navigate = () => {
2560
2682
  if (!restorationCurrent.current.active) return;
2561
- const key = JSON.stringify(new URL(window.location.href).searchParams.getAll(parameter));
2683
+ const key = tableUrlKey(window.location.href);
2562
2684
  if (key === observedQueryUrl.current) return;
2563
2685
  observedQueryUrl.current = key;
2564
2686
  void restore(true).catch(() => {
@@ -2618,7 +2740,7 @@ function useQueryTable(opts) {
2618
2740
  const definitionKey = JSON.stringify(computedRevisions);
2619
2741
  const queryKey = useMemo9(() => JSON.stringify(query), [query]);
2620
2742
  const serverRowQuery = useMemo9(() => {
2621
- const execution = opts.computedExecution;
2743
+ const execution2 = opts.computedExecution;
2622
2744
  const detailConfig = typeof opts.itemDetails === "object" ? opts.itemDetails : void 0;
2623
2745
  const page = query.itemDetails?.page ?? detailConfig?.page;
2624
2746
  const pageEnabled = Boolean(opts.itemDetails) && (query.itemDetails?.modes ?? initialDetailModes(detailConfig)).includes("page") && (!detailConfig?.modes || detailConfig.modes.includes("page"));
@@ -2627,13 +2749,13 @@ function useQueryTable(opts) {
2627
2749
  ...query.select.length ? query.select : defaults.select,
2628
2750
  ...pageFields.filter((name) => displaySchema.fields.some((field) => field.name === name)).map((field) => ({ field }))
2629
2751
  ] } : executionQuery;
2630
- const v2Request = execution && transport?.fetchRowsV2 ? toServerQueryV22(requestQuery, displaySchema, execution) : void 0;
2752
+ const v2Request = execution2 && transport?.fetchRowsV2 ? toServerQueryV22(requestQuery, displaySchema, execution2) : void 0;
2631
2753
  const request = v2Request ?? toServerQuery2(requestQuery, displaySchema);
2632
2754
  if (v2Request)
2633
2755
  for (const definition of computed.definitions) {
2634
- if ((execution?.fields[computedFieldName2(definition.id)]?.select || v2Request.orderBy.some(
2756
+ if ((execution2?.fields[computedFieldName2(definition.id)]?.select || v2Request.orderBy.some(
2635
2757
  (term) => term.field === computedFieldName2(definition.id)
2636
- )) && executionRevision(execution, definition.id) !== void 0 && executionRevision(execution, definition.id) !== definition.revision)
2758
+ )) && executionRevision(execution2, definition.id) !== void 0 && executionRevision(execution2, definition.id) !== definition.revision)
2637
2759
  v2Request.diagnostics.push({
2638
2760
  from: 0,
2639
2761
  to: 1,
@@ -2644,7 +2766,7 @@ function useQueryTable(opts) {
2644
2766
  for (const clause of query.aggregations ?? [])
2645
2767
  if (clause.scope === "shownRows") {
2646
2768
  try {
2647
- for (const name of metricDependencies(
2769
+ for (const name of metricDependencies2(
2648
2770
  clause,
2649
2771
  schema,
2650
2772
  resolveComputed
@@ -2694,7 +2816,7 @@ function useQueryTable(opts) {
2694
2816
  ]
2695
2817
  );
2696
2818
  const executionKey = JSON.stringify(opts.computedExecution ?? null);
2697
- const pageContext = `${schema.name}:${rowQueryKey}:${nonce}:${executionKey}`;
2819
+ const pageContext = `${schema.name}:${rowQueryKey}:${nonce}:${executionKey}:${hostStateKey}`;
2698
2820
  useEffect10(() => {
2699
2821
  if (!active || !storageReady || restoration.loading || restoration.error) {
2700
2822
  setRows((previous) => previous.length ? [] : previous);
@@ -2708,10 +2830,11 @@ function useQueryTable(opts) {
2708
2830
  const run = async () => {
2709
2831
  setLoading(true);
2710
2832
  setError(null);
2833
+ setFailedPage(null);
2711
2834
  try {
2712
2835
  validateExecution(query);
2713
2836
  if (transport) {
2714
- 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))) || !transport.fetchRows && !(transport.fetchRowsV2 && opts.computedExecution))) {
2837
+ 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) => predicatesOf6(term).some((predicate) => isComputedField4(predicate.field))) || !transport.fetchRows && !(transport.fetchRowsV2 && opts.computedExecution))) {
2715
2838
  const request = toQueryRequestV32(
2716
2839
  { ...query, aggregations: [], select: [
2717
2840
  ...query.select.filter((column) => isComputedField4(column.field)),
@@ -2792,6 +2915,7 @@ function useQueryTable(opts) {
2792
2915
  } catch (e) {
2793
2916
  if (!cancelled && !ac.signal.aborted) {
2794
2917
  setError(e);
2918
+ setFailedPage(pageContext);
2795
2919
  setRows([]);
2796
2920
  setTotal(null);
2797
2921
  setCommittedPage(null);
@@ -2820,11 +2944,13 @@ function useQueryTable(opts) {
2820
2944
  active,
2821
2945
  storageReady
2822
2946
  ]);
2947
+ const persistLast = opts.persistLast ?? "always";
2948
+ const hostStorageUnsupported = Boolean(host) && !storage.storesHostState;
2823
2949
  useEffect10(() => {
2824
- if (!storageReady || restoration.loading || restoration.error) return;
2950
+ if (persistLast !== "always" || !storageReady || restoration.loading || restoration.error || hostStorageUnsupported) return;
2825
2951
  const t = setTimeout(() => void storage.saveLast(sourceSchema.name, query), 400);
2826
2952
  return () => clearTimeout(t);
2827
- }, [queryKey, storage, sourceSchema.name, query, restoration, storageReady]);
2953
+ }, [persistLast, queryKey, storage, sourceSchema.name, query, restoration, storageReady, hostStorageUnsupported]);
2828
2954
  const idField = useMemo9(
2829
2955
  () => schema.fields.find((f) => f.name === schema.idField),
2830
2956
  [schema]
@@ -2845,7 +2971,7 @@ function useQueryTable(opts) {
2845
2971
  useEffect10(() => {
2846
2972
  selection.clear();
2847
2973
  }, [identityScope, selection.clear]);
2848
- const columnSchema = useMemo9(() => detailColumnSchema(displaySchema, detailsEnabled), [displaySchema, detailsEnabled]);
2974
+ const columnSchema = useMemo9(() => detailColumnSchema(displaySchema, detailsEnabled || groupDetailsEnabled), [displaySchema, detailsEnabled, groupDetailsEnabled]);
2849
2975
  const resultDefaults = useMemo9(() => query.grouping ? defaultsFor(schema) : defaults, [Boolean(query.grouping), schema, defaults]);
2850
2976
  const select = useSelect(query, setQuery, columnSchema);
2851
2977
  const columnDrag = useColumnDrag();
@@ -2853,7 +2979,7 @@ function useQueryTable(opts) {
2853
2979
  sourceSchema.name,
2854
2980
  query,
2855
2981
  (q) => {
2856
- const normalized = normalizeQueryState3(q, defaults);
2982
+ const normalized = withHostState(normalizeQueryState3(q, defaults), hostRef.current);
2857
2983
  savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(normalized) };
2858
2984
  viewRevision.current++;
2859
2985
  setQuery(normalized);
@@ -2862,11 +2988,23 @@ function useQueryTable(opts) {
2862
2988
  now,
2863
2989
  links.snapshot
2864
2990
  );
2865
- const saved = { ...savedQueries, save: async (name) => {
2991
+ const hostStorageError = host && !storage.storesHostState ? "This storage adapter cannot save host query state (set StorageAdapter.storesHostState once it keeps QueryState.hostState)." : null;
2992
+ const savedItems = useMemo9(() => !host ? savedQueries.items : savedQueries.items.map((item) => {
2993
+ try {
2994
+ return { ...item, query: withHostState(normalizeQueryState3(item.query, defaults), hostRef.current) };
2995
+ } catch {
2996
+ return item;
2997
+ }
2998
+ }), [savedQueries.items, Boolean(host), defaults]);
2999
+ const saved = { ...savedQueries, items: savedItems, save: async (name) => {
3000
+ if (hostStorageError) throw Error(hostStorageError);
2866
3001
  const item = await savedQueries.save(name);
2867
- savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(normalizeQueryState3(item.query, defaults)) };
3002
+ savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(withHostState(normalizeQueryState3(item.query, defaults), hostRef.current)) };
2868
3003
  viewRevision.current++;
2869
3004
  return item;
3005
+ }, update: async (id) => {
3006
+ if (hostStorageError) throw Error(hostStorageError);
3007
+ return savedQueries.update(id);
2870
3008
  } };
2871
3009
  const pageReady = committedPage === pageContext && committedSource?.schema === schema && committedSource?.transport === transport && committedSource?.clientRows === clientRows && !error;
2872
3010
  const dataToken = useRef9(0);
@@ -2886,7 +3024,7 @@ function useQueryTable(opts) {
2886
3024
  const capabilities = metricTransport.queryCapabilities;
2887
3025
  const refused = /* @__PURE__ */ new Map();
2888
3026
  for (const clause of metricQuery.aggregations ?? []) {
2889
- const issue = metricPopulationError2(clause, { filters: capabilities.metricFilters, minCount: capabilities.metricMinCount });
3027
+ const issue = metricPopulationError2(clause, { filters: capabilities.metricFilters, minCount: capabilities.metricMinCount, minCountMeasures: capabilities.metricMinCountMeasures });
2890
3028
  if (issue) refused.set(clause.id, issue);
2891
3029
  }
2892
3030
  const runnable = (metricQuery.aggregations ?? []).filter((clause) => !refused.has(clause.id));
@@ -2938,7 +3076,7 @@ function useQueryTable(opts) {
2938
3076
  nonce,
2939
3077
  validateExecution,
2940
3078
  {
2941
- ...metricTransport?.fetchQueryV3 && metricTransport.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 } : {},
3079
+ ...metricTransport?.fetchQueryV3 && metricTransport.queryCapabilities?.version === 3 && (query.grouping || query.aggregations?.some((metric) => metric.dateGrouping) || query.where.some((term) => predicatesOf6(term).some((predicate) => isComputedField4(predicate.field)))) ? { executeV3: executeV3Metrics, queryContextKey: groupKey } : {},
2942
3080
  ...groupingNow !== void 0 ? { now: groupingNow, strictLocalMetrics: true } : {},
2943
3081
  shownRows: rows,
2944
3082
  rowsReady: metricRowsReady,
@@ -3239,6 +3377,62 @@ function useQueryTable(opts) {
3239
3377
  query.where
3240
3378
  ]
3241
3379
  );
3380
+ const executionIdentity = JSON.stringify([pageContext, query.aggregations ?? [], query.where, groupKey, nonce]);
3381
+ const executionGeneration = useRef9({ key: "", generation: 0 });
3382
+ if (executionGeneration.current.key !== executionIdentity)
3383
+ executionGeneration.current = { key: executionIdentity, generation: executionGeneration.current.generation + 1 };
3384
+ const execution = useMemo9(() => {
3385
+ if (!active || !storageReady || restoration.loading || restoration.error) return null;
3386
+ const rowsStatus = pageReady ? "succeeded" : error && failedPage === pageContext ? "failed" : "running";
3387
+ const metricError = aggState.error ?? aggState.results?.metrics.find((entry) => entry.error)?.error;
3388
+ const metricsStatus = !query.aggregations?.length ? "none" : aggState.loading || !aggState.results && !aggState.error ? "running" : metricError ? "failed" : "succeeded";
3389
+ const status = rowsStatus === "failed" || metricsStatus === "failed" ? "failed" : rowsStatus === "succeeded" && metricsStatus !== "running" ? "succeeded" : "running";
3390
+ return {
3391
+ query,
3392
+ key: executionIdentity,
3393
+ generation: executionGeneration.current.generation,
3394
+ status,
3395
+ rows: rowsStatus,
3396
+ metrics: metricsStatus,
3397
+ error: rowsStatus === "failed" ? error : metricError ? metricError instanceof Error ? metricError : Error(String(metricError)) : null
3398
+ };
3399
+ }, [
3400
+ active,
3401
+ storageReady,
3402
+ restoration.loading,
3403
+ restoration.error,
3404
+ pageReady,
3405
+ error,
3406
+ failedPage,
3407
+ pageContext,
3408
+ aggState.error,
3409
+ aggState.results,
3410
+ aggState.loading,
3411
+ query,
3412
+ executionIdentity
3413
+ ]);
3414
+ const succeededQuery = execution?.status === "succeeded" ? execution.query : null;
3415
+ const pendingLast = useRef9(null);
3416
+ const savedLast = useRef9(null);
3417
+ useEffect10(() => {
3418
+ if (persistLast !== "succeeded" || !succeededQuery || hostStorageUnsupported) return;
3419
+ const key = JSON.stringify([sourceSchema.name, succeededQuery]);
3420
+ if (pendingLast.current?.key === key || !pendingLast.current && savedLast.current === key) return;
3421
+ if (pendingLast.current) clearTimeout(pendingLast.current.timer);
3422
+ const save = () => {
3423
+ pendingLast.current = null;
3424
+ savedLast.current = key;
3425
+ void storage.saveLast(sourceSchema.name, succeededQuery);
3426
+ };
3427
+ pendingLast.current = { key, save, timer: setTimeout(save, 400) };
3428
+ }, [persistLast, succeededQuery, storage, sourceSchema.name, hostStorageUnsupported]);
3429
+ useEffect10(() => () => {
3430
+ const pending = pendingLast.current;
3431
+ if (pending) {
3432
+ clearTimeout(pending.timer);
3433
+ pending.save();
3434
+ }
3435
+ }, []);
3242
3436
  const sourceRowId = useCallback12((row) => {
3243
3437
  const field = sourceSchema.fields.find((f) => f.name === sourceSchema.idField);
3244
3438
  const value = field ? readFieldValue4(field, row) : row[sourceSchema.idField];
@@ -3258,16 +3452,71 @@ function useQueryTable(opts) {
3258
3452
  queryParameter: parameter,
3259
3453
  queryReady: storageReady && !restoration.loading && !restoration.error
3260
3454
  });
3455
+ const groupPopulation = query.grouping ? JSON.stringify([query.grouping, query.where, nonce, hostStateKey]) : "";
3456
+ const groupRecords = useRef9({ population: "", source: [], rows: /* @__PURE__ */ new Map() });
3457
+ const groupSource = [clientRows, transport, schema];
3458
+ if (groupRecords.current.population !== groupPopulation || groupSource.some((part, i) => part !== groupRecords.current.source[i]))
3459
+ groupRecords.current = { population: groupPopulation, source: groupSource, rows: /* @__PURE__ */ new Map() };
3460
+ const groupsSettled = groupDetailsEnabled && pageReady && !restoration.loading;
3461
+ if (groupsSettled) for (const row of rows) {
3462
+ const id = rowId(row);
3463
+ if (id != null) groupRecords.current.rows.set(id, row);
3464
+ }
3465
+ const groupDetailOptions = useMemo9(() => {
3466
+ if (!groupDetailsEnabled) return void 0;
3467
+ const config = typeof opts.itemDetails === "object" ? opts.itemDetails : void 0;
3468
+ const modes = (config?.modes ?? ["hover", "modal", "expand"]).filter((mode) => mode !== "page");
3469
+ return {
3470
+ modes,
3471
+ defaultModes: initialDetailModes(config).filter((mode) => modes.includes(mode)),
3472
+ objectType: `${config?.objectType ?? sourceSchema.name}:groups`,
3473
+ ...config?.storage ? { storage: config.storage } : {}
3474
+ };
3475
+ }, [groupDetailsEnabled, opts.itemDetails, sourceSchema.name]);
3476
+ const groupDetailSchema = useMemo9(() => ({
3477
+ ...displaySchema,
3478
+ defaultSelect: displaySchema.fields.filter((field) => field.name !== displaySchema.idField).map((field) => ({ field: field.name })),
3479
+ fields: displaySchema.fields.map((field) => field.name === displaySchema.idField ? { ...field, property: { enabled: false } } : field)
3480
+ }), [displaySchema]);
3481
+ const setGroupQuery = useCallback12((next) => setQuery((current) => current.grouping ? typeof next === "function" ? next(current) : next : current), [setQuery]);
3482
+ const groupDetails = useItemDetails({
3483
+ schema: groupDetailSchema,
3484
+ query,
3485
+ setQuery: setGroupQuery,
3486
+ options: groupDetailOptions,
3487
+ rows,
3488
+ clientRows: void 0,
3489
+ rowId,
3490
+ transport: void 0,
3491
+ refreshKey: nonce,
3492
+ links,
3493
+ queryParameter: parameter,
3494
+ queryReady: storageReady && !restoration.loading && !restoration.error,
3495
+ lookupRow: (id) => {
3496
+ const row = groupRecords.current.rows.get(id);
3497
+ return row ? { row } : groupsSettled || error ? null : void 0;
3498
+ },
3499
+ subject: "group"
3500
+ });
3261
3501
  const api = {
3262
3502
  restoration: {
3263
3503
  status: restoration.error ? "error" : !storageReady || restoration.loading ? "resolving" : "ready",
3264
3504
  ready: active && storageReady && !restoration.loading && !restoration.error,
3265
3505
  error: restoration.error
3266
3506
  },
3507
+ execution,
3508
+ ...host && query.hostState ? {
3509
+ definition: { query: withoutHostState(query), hostState: query.hostState },
3510
+ setDefinition: (next) => {
3511
+ const current = queryHistoryRef.current.query;
3512
+ const value = typeof next === "function" ? next({ query: withoutHostState(current), hostState: current.hostState }) : next;
3513
+ setQuery(withHostState({ ...normalizeQueryState3(value.query, defaults), hostState: value.hostState }, hostRef.current));
3514
+ }
3515
+ } : {},
3267
3516
  restoreLocation,
3268
3517
  links,
3269
3518
  schema: columnSchema,
3270
- ...itemDetails && detailsEnabled ? { itemDetails } : {},
3519
+ ...itemDetails && detailsEnabled ? { itemDetails } : groupDetails ? { itemDetails: groupDetails } : {},
3271
3520
  query,
3272
3521
  defaults: resultDefaults,
3273
3522
  setQuery,
@@ -3310,6 +3559,25 @@ function useQueryTable(opts) {
3310
3559
  visibleFields,
3311
3560
  rowId
3312
3561
  };
3562
+ const sourceFilterValues = useCallback12(async (name, search) => {
3563
+ if (metadataTransport?.fetchDistinctValues) return metadataTransport.fetchDistinctValues({ field: name, search });
3564
+ const field = groupingModel.sourceField(name);
3565
+ return field && sourceRows ? { ...distinctValuesFromRows(field, sourceRows, search), hasNull: fieldHasNullInRows(field, sourceRows) } : { values: [], hasMore: false };
3566
+ }, [metadataTransport, groupingModel, sourceRows]);
3567
+ const sourceFieldStats = useCallback12(async (names, signal) => {
3568
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
3569
+ const requested = [...new Set(names)];
3570
+ if (metadataTransport?.fetchFieldStats) {
3571
+ const backendFields = requested.filter(
3572
+ (name) => sourceCatalogue.fields.find((field) => field.name === name)?.source.kind === "backend"
3573
+ );
3574
+ return backendFields.length ? metadataTransport.fetchFieldStats(backendFields, signal) : {};
3575
+ }
3576
+ return Object.fromEntries(requested.flatMap((name) => {
3577
+ const field = groupingModel.sourceField(name);
3578
+ return field && sourceRows ? [[name, { distinct: new Set(sourceRows.map((row) => JSON.stringify(readFieldValue4(field, row)))).size }]] : [];
3579
+ }));
3580
+ }, [metadataTransport, groupingModel, sourceRows, sourceCatalogue]);
3313
3581
  const sourceApi = {
3314
3582
  ...sourceFilters,
3315
3583
  loading,
@@ -3318,25 +3586,8 @@ function useQueryTable(opts) {
3318
3586
  setQuery: setSourceQuery,
3319
3587
  ...itemDetails ? { itemDetails } : {},
3320
3588
  schema: sourceCatalogue,
3321
- filterValues: async (name, search) => {
3322
- if (metadataTransport?.fetchDistinctValues) return metadataTransport.fetchDistinctValues({ field: name, search });
3323
- const field = groupingModel.sourceField(name);
3324
- return field && sourceRows ? { ...distinctValuesFromRows(field, sourceRows, search), hasNull: fieldHasNullInRows(field, sourceRows) } : { values: [], hasMore: false };
3325
- },
3326
- fieldStats: async (names, signal) => {
3327
- if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
3328
- const requested = [...new Set(names)];
3329
- if (metadataTransport?.fetchFieldStats) {
3330
- const backendFields = requested.filter(
3331
- (name) => sourceCatalogue.fields.find((field) => field.name === name)?.source.kind === "backend"
3332
- );
3333
- return backendFields.length ? metadataTransport.fetchFieldStats(backendFields, signal) : {};
3334
- }
3335
- return Object.fromEntries(requested.flatMap((name) => {
3336
- const field = groupingModel.sourceField(name);
3337
- return field && sourceRows ? [[name, { distinct: new Set(sourceRows.map((row) => JSON.stringify(readFieldValue4(field, row)))).size }]] : [];
3338
- }));
3339
- }
3589
+ filterValues: sourceFilterValues,
3590
+ fieldStats: sourceFieldStats
3340
3591
  };
3341
3592
  const grouping = useGroupingController(
3342
3593
  api,
@@ -3350,7 +3601,7 @@ function useQueryTable(opts) {
3350
3601
  return {
3351
3602
  ...api,
3352
3603
  ...groupingSupported || query.grouping ? { grouping } : {},
3353
- select: { ...select, hide: (field) => query.grouping?.keys.includes(field) ? grouping.changeKeys(query.grouping.keys.filter((key) => key !== field)) : select.hide(field) }
3604
+ select: { ...select, hide: (field) => query.grouping && groupKeyFields2(query.grouping.keys).includes(field) ? grouping.changeKeys(query.grouping.keys.filter((key) => groupKeyField(key) !== field)) : select.hide(field) }
3354
3605
  };
3355
3606
  }
3356
3607
  function nextOrderBy(existing, field, additive) {