@pythia-software/query-table-react 0.9.5 → 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,9 +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
- predicatesOf as predicatesOf3,
1435
+ hasMetricSeries,
1436
+ planMetricSeries,
1437
+ predicatesOf as predicatesOf5,
1391
1438
  toAggregationQuery,
1392
1439
  toMetricQuery,
1393
1440
  validateMetric,
@@ -1405,7 +1452,7 @@ function usesLocalMetricPage(clause, context) {
1405
1452
  async function localMetrics(rows, query, context, signal) {
1406
1453
  const originalClauses = query.aggregations ?? [];
1407
1454
  const advanced = (query.aggregations ?? []).filter(
1408
- (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 || c.where !== void 0 || c.minCount !== void 0
1455
+ (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 || c.where !== void 0 || c.minCount !== void 0 || hasMetricSeries(c)
1409
1456
  );
1410
1457
  if (advanced.length && (originalClauses.length > 20 || normalizeQueryState2(query).aggregations?.length !== originalClauses.length))
1411
1458
  return evaluateMetrics(rows, query, context.schema, {
@@ -1515,6 +1562,16 @@ async function executeMetrics(query, context, signal) {
1515
1562
  abort(signal);
1516
1563
  const clauses = query.aggregations ?? [];
1517
1564
  if (!clauses.length) return { metrics: [] };
1565
+ const series = planMetricSeries(query, context.schema, {
1566
+ ...context.now !== void 0 ? { now: context.now } : {},
1567
+ ...context.resolveComputed ? { resolveComputed: context.resolveComputed } : {}
1568
+ });
1569
+ if (series) {
1570
+ const parts = await Promise.all(
1571
+ series.queries.map((part) => executeMetrics(part, context, signal))
1572
+ );
1573
+ return { metrics: series.combine(parts.flatMap((part) => part.metrics)) };
1574
+ }
1518
1575
  if (context.executeV3) return context.executeV3(query, signal);
1519
1576
  if (context.rowsReady === false && clauses.some((c) => usesLocalMetricPage(c, context)))
1520
1577
  throw Error("Wait for the current table page before previewing shown-row metrics.");
@@ -1533,11 +1590,10 @@ async function executeMetrics(query, context, signal) {
1533
1590
  shown.flatMap((clause) => [
1534
1591
  ...clause.groupBy,
1535
1592
  ...(clause.where ?? []).flatMap(
1536
- (term) => predicatesOf3(term).map((predicate) => predicate.field)
1593
+ (term) => predicatesOf5(term).map((predicate) => predicate.field)
1537
1594
  ),
1538
- ...metricPlans(clause, context.schema, context.resolveComputed).flatMap(
1539
- (p) => p.dependencies
1540
- )
1595
+ // Includes a measured minimum's field, which the page must also hold.
1596
+ ...metricDependencies(clause, context.schema, context.resolveComputed)
1541
1597
  ])
1542
1598
  );
1543
1599
  for (const name of names) {
@@ -2122,7 +2178,7 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now, snaps
2122
2178
  // src/useQueryLinks.ts
2123
2179
  import { useCallback as useCallback11, useEffect as useEffect9, useRef as useRef8, useState as useState10 } from "react";
2124
2180
  import { encodeQuery } from "@pythia-software/query-table-core";
2125
- function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, onUrlChange) {
2181
+ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, onUrlChange, decorate = (url) => url) {
2126
2182
  const latest = useRef8(query);
2127
2183
  latest.current = query;
2128
2184
  const [kind, setKind] = useState10(null);
@@ -2141,14 +2197,15 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2141
2197
  if (!syncUrl || !ready || typeof window === "undefined") return;
2142
2198
  try {
2143
2199
  const result = service.local(query, window.location.href, parameter);
2144
- window.history.replaceState(window.history.state, "", result.url);
2145
- onUrlChange(result.url);
2200
+ const url = decorate(result.url, query);
2201
+ window.history.replaceState(window.history.state, "", url);
2202
+ onUrlChange(url);
2146
2203
  setKind(result.kind);
2147
2204
  setError(null);
2148
2205
  } catch (e) {
2149
2206
  setError(e instanceof Error ? e : Error(String(e)));
2150
2207
  }
2151
- }, [queryKey, syncUrl, ready, service, parameter, onUrlChange]);
2208
+ }, [queryKey, syncUrl, ready, service, parameter, onUrlChange, decorate]);
2152
2209
  const snapshot = useCallback11(async (q) => {
2153
2210
  const captured = encodeQuery(q);
2154
2211
  const base = typeof window !== "undefined" ? window.location.href : void 0;
@@ -2162,8 +2219,9 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2162
2219
  const url = new URL(base);
2163
2220
  url.searchParams.set(parameter, "@" + saved.code);
2164
2221
  const result = service.local(q, url.toString(), parameter);
2165
- window.history.replaceState(window.history.state, "", result.url);
2166
- onUrlChange(result.url);
2222
+ const next = decorate(result.url, q);
2223
+ window.history.replaceState(window.history.state, "", next);
2224
+ onUrlChange(next);
2167
2225
  setKind("durable");
2168
2226
  }
2169
2227
  return saved;
@@ -2174,7 +2232,7 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2174
2232
  operations.current--;
2175
2233
  if (mounted.current) setBusy(operations.current > 0);
2176
2234
  }
2177
- }, [service, syncUrl, ready, parameter, onUrlChange]);
2235
+ }, [service, syncUrl, ready, parameter, onUrlChange, decorate]);
2178
2236
  const create = useCallback11(async (q = latest.current, options = {}) => {
2179
2237
  const baseUrl = options.baseUrl ?? window.location.href;
2180
2238
  const param = options.parameter ?? parameter;
@@ -2184,7 +2242,8 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2184
2242
  setError(null);
2185
2243
  try {
2186
2244
  if (!ready) throw Error("Resolve or reset the query link before sharing it.");
2187
- 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;
2188
2247
  if (mounted.current && syncUrl && ready && param === parameter && window.location.href === baseUrl && encodeQuery(latest.current) === captured) {
2189
2248
  window.history.replaceState(window.history.state, "", result.url);
2190
2249
  onUrlChange(result.url);
@@ -2198,8 +2257,11 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2198
2257
  operations.current--;
2199
2258
  if (mounted.current) setBusy(operations.current > 0);
2200
2259
  }
2201
- }, [service, syncUrl, ready, parameter, onUrlChange]);
2202
- 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]);
2203
2265
  return { maxUrlBytes, kind, busy, error, create, local, snapshot };
2204
2266
  }
2205
2267
 
@@ -2236,7 +2298,7 @@ function defaultsFor(schema, details = false) {
2236
2298
  });
2237
2299
  }
2238
2300
  function cloneWhereTerm(term) {
2239
- 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 };
2240
2302
  if (term.setFilter)
2241
2303
  copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
2242
2304
  return copy;
@@ -2252,6 +2314,7 @@ function cloneQueryState(q) {
2252
2314
  offset: q.offset
2253
2315
  };
2254
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]])) };
2255
2318
  if (q.aggregations)
2256
2319
  clone.aggregations = JSON.parse(
2257
2320
  JSON.stringify(q.aggregations)
@@ -2313,16 +2376,61 @@ function hasQueryLink(url, parameter) {
2313
2376
  function queryParameter(opts) {
2314
2377
  return opts.queryParameter ?? (typeof opts.itemDetails === "object" ? opts.itemDetails.queryParameter : void 0) ?? "q";
2315
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
+ }
2316
2424
  function resolveInitial(opts) {
2317
- const defaults = defaultsFor(opts.schema, Boolean(opts.itemDetails));
2425
+ const defaults = withHostState(defaultsFor(opts.schema, Boolean(opts.itemDetails)), opts.hostQuery);
2318
2426
  if (opts.initialQuery)
2319
2427
  return {
2320
- q: normalizeQueryState3(opts.initialQuery, defaults),
2428
+ q: withHostState(normalizeQueryState3(opts.initialQuery, defaults), opts.hostQuery),
2321
2429
  fromUrl: false
2322
2430
  };
2323
2431
  if (opts.syncUrl === true && opts.active !== false && typeof window !== "undefined") {
2324
2432
  const parameter = queryParameter(opts);
2325
- if (hasQueryLink(window.location.href, parameter))
2433
+ if (hasQueryLink(window.location.href, parameter) || opts.hostQuery && hostStateFromUrl(window.location.href, opts.hostQuery))
2326
2434
  return {
2327
2435
  q: defaults,
2328
2436
  fromUrl: true
@@ -2341,8 +2449,13 @@ function useQueryTable(opts) {
2341
2449
  const active = opts.active !== false;
2342
2450
  const urlSyncEnabled = opts.syncUrl === true;
2343
2451
  const syncUrl = urlSyncEnabled && active;
2344
- const { transport: metadataTransport, metricTransport, activity: requestActivity } = useRequestActivity(sourceTransport, opts.metricTransport);
2345
- 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.`);
2346
2459
  const storage = useMemo9(
2347
2460
  () => opts.storage ?? memoryStorageAdapter(),
2348
2461
  [opts.storage]
@@ -2355,7 +2468,10 @@ function useQueryTable(opts) {
2355
2468
  const now = opts.now ?? Date.now;
2356
2469
  const nowRef = useRef9(now);
2357
2470
  const onRefreshRef = useRef9(opts.onRefresh);
2358
- 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
+ );
2359
2475
  const initRef = useRef9(resolveInitial(opts));
2360
2476
  const [restoration, setRestoration] = useState11({ loading: initRef.current.fromUrl, error: null });
2361
2477
  const [storageReady, setStorageReady] = useState11(Boolean(initialQuery));
@@ -2392,6 +2508,10 @@ function useQueryTable(opts) {
2392
2508
  () => opts.canonicalizeQuery?.(storedQuery) ?? storedQuery,
2393
2509
  [storedQuery, opts.canonicalizeQuery]
2394
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;
2395
2515
  const sourceCatalogue = useMemo9(() => sourceExecutionSchema2(sourceSchema, opts.computedExecution), [sourceSchema, opts.computedExecution]);
2396
2516
  const sourceRows = providedRows;
2397
2517
  const groupingModel = useMemo9(() => createGroupingModel(sourceCatalogue), [sourceCatalogue]);
@@ -2421,6 +2541,7 @@ function useQueryTable(opts) {
2421
2541
  const executionQuery = useMemo9(() => withoutGrouping2(query), [query]);
2422
2542
  const byName = useMemo9(() => new Map(schema.fields.map((field) => [field.name, field])), [schema]);
2423
2543
  const detailsEnabled = Boolean(opts.itemDetails) && !query.grouping;
2544
+ const groupDetailsEnabled = Boolean(opts.itemDetails) && Boolean(query.grouping);
2424
2545
  const validateExecution = useCallback12((q) => {
2425
2546
  if (prepared?.error) throw prepared.error;
2426
2547
  opts.validateQuery?.(query.grouping ? { ...q, grouping: query.grouping } : q);
@@ -2433,6 +2554,7 @@ function useQueryTable(opts) {
2433
2554
  const [loading, setLoading] = useState11(false);
2434
2555
  const [error, setError] = useState11(null);
2435
2556
  const [committedPage, setCommittedPage] = useState11(null);
2557
+ const [failedPage, setFailedPage] = useState11(null);
2436
2558
  const [committedSource, setCommittedSource] = useState11(null);
2437
2559
  const rows = useMemo9(() => committedSource?.schema === schema ? committedRows : [], [committedSource?.schema, schema, committedRows]);
2438
2560
  const [autoRefreshStatus, setAutoRefreshStatus] = useState11(null);
@@ -2452,7 +2574,8 @@ function useQueryTable(opts) {
2452
2574
  updateQueryHistory((history) => {
2453
2575
  const prev = history.query;
2454
2576
  const candidate = typeof next === "function" ? next(prev) : next;
2455
- 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);
2456
2579
  if (queriesEqual2(prev, nextQuery)) return history;
2457
2580
  return { query: cloneQueryState(nextQuery), past: [...history.past, cloneQueryState(prev)], future: [] };
2458
2581
  });
@@ -2469,9 +2592,15 @@ function useQueryTable(opts) {
2469
2592
  }, [setQuery]);
2470
2593
  const sourceFilters = useQueryFilters(sourceCatalogue, setSourceQuery);
2471
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
+ };
2472
2599
  const recordQueryUrl = useCallback12((url) => {
2473
- observedQueryUrl.current = JSON.stringify(new URL(url).searchParams.getAll(parameter));
2600
+ observedQueryUrl.current = tableUrlKey(url);
2474
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]);
2475
2604
  const links = useQueryLinks(
2476
2605
  query,
2477
2606
  linkService,
@@ -2479,7 +2608,8 @@ function useQueryTable(opts) {
2479
2608
  storageReady && !restoration.loading && !restoration.error,
2480
2609
  parameter,
2481
2610
  opts.links?.maxUrlBytes ?? 2e3,
2482
- recordQueryUrl
2611
+ recordQueryUrl,
2612
+ decorateLink
2483
2613
  );
2484
2614
  const restoreSources = useRef9({ storage, linkService, defaults, initialQuery });
2485
2615
  restoreSources.current = { storage, linkService, defaults, initialQuery };
@@ -2501,17 +2631,21 @@ function useQueryTable(opts) {
2501
2631
  signal?.addEventListener("abort", abort2, { once: true });
2502
2632
  if (signal?.aborted) currentController.abort();
2503
2633
  setStorageReady(false);
2504
- 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);
2505
2637
  setRestoration({ loading: true, error: null });
2506
2638
  try {
2507
2639
  if (currentController.signal.aborted) throw new DOMException("Query restoration cancelled.", "AbortError");
2508
2640
  const fromUrl = location && (navigation || urlSyncEnabled) ? await linkService2.resolve(location, parameter, currentController.signal) : null;
2509
- const defaultSaved = fromUrl ? null : await storage2.loadDefaultSaved?.(sourceSchema.name);
2510
- 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));
2511
2644
  if (currentController.signal.aborted) throw new DOMException("Query restoration cancelled.", "AbortError");
2645
+ if (linked && !fromUrl?.hostState && hostFromUrl) restored = { ...linked, hostState: hostFromUrl };
2512
2646
  if (!cancelled && run === generation && epoch === manualQueryEpoch.current) {
2513
2647
  if (restored) {
2514
- const normalized = normalizeQueryState3(restored, defaults2);
2648
+ const normalized = withHostState(normalizeQueryState3(restored, defaults2), host2);
2515
2649
  savedBaseline.current = defaultSaved || fromUrl?.grouping ? { source: sourceSchema.name, query: cloneQueryState(normalized) } : null;
2516
2650
  viewRevision.current++;
2517
2651
  updateQueryHistory(() => ({ query: normalized, past: [], future: [] }));
@@ -2536,7 +2670,7 @@ function useQueryTable(opts) {
2536
2670
  }
2537
2671
  restoreHandler.current = async (location, signal) => {
2538
2672
  location = new URL(location, typeof window !== "undefined" ? window.location.href : void 0).href;
2539
- const key = JSON.stringify(new URL(location).searchParams.getAll(parameter));
2673
+ const key = tableUrlKey(location);
2540
2674
  const current = restorationCurrent.current;
2541
2675
  if (key === observedQueryUrl.current && current.storageReady && !current.restoration.loading && !current.restoration.error) return;
2542
2676
  recordQueryUrl(location);
@@ -2546,7 +2680,7 @@ function useQueryTable(opts) {
2546
2680
  if (typeof window !== "undefined") recordQueryUrl(window.location.href);
2547
2681
  const navigate = () => {
2548
2682
  if (!restorationCurrent.current.active) return;
2549
- const key = JSON.stringify(new URL(window.location.href).searchParams.getAll(parameter));
2683
+ const key = tableUrlKey(window.location.href);
2550
2684
  if (key === observedQueryUrl.current) return;
2551
2685
  observedQueryUrl.current = key;
2552
2686
  void restore(true).catch(() => {
@@ -2606,7 +2740,7 @@ function useQueryTable(opts) {
2606
2740
  const definitionKey = JSON.stringify(computedRevisions);
2607
2741
  const queryKey = useMemo9(() => JSON.stringify(query), [query]);
2608
2742
  const serverRowQuery = useMemo9(() => {
2609
- const execution = opts.computedExecution;
2743
+ const execution2 = opts.computedExecution;
2610
2744
  const detailConfig = typeof opts.itemDetails === "object" ? opts.itemDetails : void 0;
2611
2745
  const page = query.itemDetails?.page ?? detailConfig?.page;
2612
2746
  const pageEnabled = Boolean(opts.itemDetails) && (query.itemDetails?.modes ?? initialDetailModes(detailConfig)).includes("page") && (!detailConfig?.modes || detailConfig.modes.includes("page"));
@@ -2615,13 +2749,13 @@ function useQueryTable(opts) {
2615
2749
  ...query.select.length ? query.select : defaults.select,
2616
2750
  ...pageFields.filter((name) => displaySchema.fields.some((field) => field.name === name)).map((field) => ({ field }))
2617
2751
  ] } : executionQuery;
2618
- const v2Request = execution && transport?.fetchRowsV2 ? toServerQueryV22(requestQuery, displaySchema, execution) : void 0;
2752
+ const v2Request = execution2 && transport?.fetchRowsV2 ? toServerQueryV22(requestQuery, displaySchema, execution2) : void 0;
2619
2753
  const request = v2Request ?? toServerQuery2(requestQuery, displaySchema);
2620
2754
  if (v2Request)
2621
2755
  for (const definition of computed.definitions) {
2622
- if ((execution?.fields[computedFieldName2(definition.id)]?.select || v2Request.orderBy.some(
2756
+ if ((execution2?.fields[computedFieldName2(definition.id)]?.select || v2Request.orderBy.some(
2623
2757
  (term) => term.field === computedFieldName2(definition.id)
2624
- )) && 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)
2625
2759
  v2Request.diagnostics.push({
2626
2760
  from: 0,
2627
2761
  to: 1,
@@ -2632,7 +2766,7 @@ function useQueryTable(opts) {
2632
2766
  for (const clause of query.aggregations ?? [])
2633
2767
  if (clause.scope === "shownRows") {
2634
2768
  try {
2635
- for (const name of metricDependencies(
2769
+ for (const name of metricDependencies2(
2636
2770
  clause,
2637
2771
  schema,
2638
2772
  resolveComputed
@@ -2682,7 +2816,7 @@ function useQueryTable(opts) {
2682
2816
  ]
2683
2817
  );
2684
2818
  const executionKey = JSON.stringify(opts.computedExecution ?? null);
2685
- const pageContext = `${schema.name}:${rowQueryKey}:${nonce}:${executionKey}`;
2819
+ const pageContext = `${schema.name}:${rowQueryKey}:${nonce}:${executionKey}:${hostStateKey}`;
2686
2820
  useEffect10(() => {
2687
2821
  if (!active || !storageReady || restoration.loading || restoration.error) {
2688
2822
  setRows((previous) => previous.length ? [] : previous);
@@ -2696,10 +2830,11 @@ function useQueryTable(opts) {
2696
2830
  const run = async () => {
2697
2831
  setLoading(true);
2698
2832
  setError(null);
2833
+ setFailedPage(null);
2699
2834
  try {
2700
2835
  validateExecution(query);
2701
2836
  if (transport) {
2702
- 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))) {
2703
2838
  const request = toQueryRequestV32(
2704
2839
  { ...query, aggregations: [], select: [
2705
2840
  ...query.select.filter((column) => isComputedField4(column.field)),
@@ -2780,6 +2915,7 @@ function useQueryTable(opts) {
2780
2915
  } catch (e) {
2781
2916
  if (!cancelled && !ac.signal.aborted) {
2782
2917
  setError(e);
2918
+ setFailedPage(pageContext);
2783
2919
  setRows([]);
2784
2920
  setTotal(null);
2785
2921
  setCommittedPage(null);
@@ -2808,11 +2944,13 @@ function useQueryTable(opts) {
2808
2944
  active,
2809
2945
  storageReady
2810
2946
  ]);
2947
+ const persistLast = opts.persistLast ?? "always";
2948
+ const hostStorageUnsupported = Boolean(host) && !storage.storesHostState;
2811
2949
  useEffect10(() => {
2812
- if (!storageReady || restoration.loading || restoration.error) return;
2950
+ if (persistLast !== "always" || !storageReady || restoration.loading || restoration.error || hostStorageUnsupported) return;
2813
2951
  const t = setTimeout(() => void storage.saveLast(sourceSchema.name, query), 400);
2814
2952
  return () => clearTimeout(t);
2815
- }, [queryKey, storage, sourceSchema.name, query, restoration, storageReady]);
2953
+ }, [persistLast, queryKey, storage, sourceSchema.name, query, restoration, storageReady, hostStorageUnsupported]);
2816
2954
  const idField = useMemo9(
2817
2955
  () => schema.fields.find((f) => f.name === schema.idField),
2818
2956
  [schema]
@@ -2833,7 +2971,7 @@ function useQueryTable(opts) {
2833
2971
  useEffect10(() => {
2834
2972
  selection.clear();
2835
2973
  }, [identityScope, selection.clear]);
2836
- const columnSchema = useMemo9(() => detailColumnSchema(displaySchema, detailsEnabled), [displaySchema, detailsEnabled]);
2974
+ const columnSchema = useMemo9(() => detailColumnSchema(displaySchema, detailsEnabled || groupDetailsEnabled), [displaySchema, detailsEnabled, groupDetailsEnabled]);
2837
2975
  const resultDefaults = useMemo9(() => query.grouping ? defaultsFor(schema) : defaults, [Boolean(query.grouping), schema, defaults]);
2838
2976
  const select = useSelect(query, setQuery, columnSchema);
2839
2977
  const columnDrag = useColumnDrag();
@@ -2841,7 +2979,7 @@ function useQueryTable(opts) {
2841
2979
  sourceSchema.name,
2842
2980
  query,
2843
2981
  (q) => {
2844
- const normalized = normalizeQueryState3(q, defaults);
2982
+ const normalized = withHostState(normalizeQueryState3(q, defaults), hostRef.current);
2845
2983
  savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(normalized) };
2846
2984
  viewRevision.current++;
2847
2985
  setQuery(normalized);
@@ -2850,11 +2988,23 @@ function useQueryTable(opts) {
2850
2988
  now,
2851
2989
  links.snapshot
2852
2990
  );
2853
- 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);
2854
3001
  const item = await savedQueries.save(name);
2855
- 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)) };
2856
3003
  viewRevision.current++;
2857
3004
  return item;
3005
+ }, update: async (id) => {
3006
+ if (hostStorageError) throw Error(hostStorageError);
3007
+ return savedQueries.update(id);
2858
3008
  } };
2859
3009
  const pageReady = committedPage === pageContext && committedSource?.schema === schema && committedSource?.transport === transport && committedSource?.clientRows === clientRows && !error;
2860
3010
  const dataToken = useRef9(0);
@@ -2874,7 +3024,7 @@ function useQueryTable(opts) {
2874
3024
  const capabilities = metricTransport.queryCapabilities;
2875
3025
  const refused = /* @__PURE__ */ new Map();
2876
3026
  for (const clause of metricQuery.aggregations ?? []) {
2877
- const issue = metricPopulationError2(clause, { filters: capabilities.metricFilters, minCount: capabilities.metricMinCount });
3027
+ const issue = metricPopulationError2(clause, { filters: capabilities.metricFilters, minCount: capabilities.metricMinCount, minCountMeasures: capabilities.metricMinCountMeasures });
2878
3028
  if (issue) refused.set(clause.id, issue);
2879
3029
  }
2880
3030
  const runnable = (metricQuery.aggregations ?? []).filter((clause) => !refused.has(clause.id));
@@ -2926,7 +3076,7 @@ function useQueryTable(opts) {
2926
3076
  nonce,
2927
3077
  validateExecution,
2928
3078
  {
2929
- ...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 } : {},
2930
3080
  ...groupingNow !== void 0 ? { now: groupingNow, strictLocalMetrics: true } : {},
2931
3081
  shownRows: rows,
2932
3082
  rowsReady: metricRowsReady,
@@ -3227,6 +3377,62 @@ function useQueryTable(opts) {
3227
3377
  query.where
3228
3378
  ]
3229
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
+ }, []);
3230
3436
  const sourceRowId = useCallback12((row) => {
3231
3437
  const field = sourceSchema.fields.find((f) => f.name === sourceSchema.idField);
3232
3438
  const value = field ? readFieldValue4(field, row) : row[sourceSchema.idField];
@@ -3246,16 +3452,71 @@ function useQueryTable(opts) {
3246
3452
  queryParameter: parameter,
3247
3453
  queryReady: storageReady && !restoration.loading && !restoration.error
3248
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
+ });
3249
3501
  const api = {
3250
3502
  restoration: {
3251
3503
  status: restoration.error ? "error" : !storageReady || restoration.loading ? "resolving" : "ready",
3252
3504
  ready: active && storageReady && !restoration.loading && !restoration.error,
3253
3505
  error: restoration.error
3254
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
+ } : {},
3255
3516
  restoreLocation,
3256
3517
  links,
3257
3518
  schema: columnSchema,
3258
- ...itemDetails && detailsEnabled ? { itemDetails } : {},
3519
+ ...itemDetails && detailsEnabled ? { itemDetails } : groupDetails ? { itemDetails: groupDetails } : {},
3259
3520
  query,
3260
3521
  defaults: resultDefaults,
3261
3522
  setQuery,
@@ -3298,6 +3559,25 @@ function useQueryTable(opts) {
3298
3559
  visibleFields,
3299
3560
  rowId
3300
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]);
3301
3581
  const sourceApi = {
3302
3582
  ...sourceFilters,
3303
3583
  loading,
@@ -3306,25 +3586,8 @@ function useQueryTable(opts) {
3306
3586
  setQuery: setSourceQuery,
3307
3587
  ...itemDetails ? { itemDetails } : {},
3308
3588
  schema: sourceCatalogue,
3309
- filterValues: async (name, search) => {
3310
- if (metadataTransport?.fetchDistinctValues) return metadataTransport.fetchDistinctValues({ field: name, search });
3311
- const field = groupingModel.sourceField(name);
3312
- return field && sourceRows ? { ...distinctValuesFromRows(field, sourceRows, search), hasNull: fieldHasNullInRows(field, sourceRows) } : { values: [], hasMore: false };
3313
- },
3314
- fieldStats: async (names, signal) => {
3315
- if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
3316
- const requested = [...new Set(names)];
3317
- if (metadataTransport?.fetchFieldStats) {
3318
- const backendFields = requested.filter(
3319
- (name) => sourceCatalogue.fields.find((field) => field.name === name)?.source.kind === "backend"
3320
- );
3321
- return backendFields.length ? metadataTransport.fetchFieldStats(backendFields, signal) : {};
3322
- }
3323
- return Object.fromEntries(requested.flatMap((name) => {
3324
- const field = groupingModel.sourceField(name);
3325
- return field && sourceRows ? [[name, { distinct: new Set(sourceRows.map((row) => JSON.stringify(readFieldValue4(field, row)))).size }]] : [];
3326
- }));
3327
- }
3589
+ filterValues: sourceFilterValues,
3590
+ fieldStats: sourceFieldStats
3328
3591
  };
3329
3592
  const grouping = useGroupingController(
3330
3593
  api,
@@ -3338,7 +3601,7 @@ function useQueryTable(opts) {
3338
3601
  return {
3339
3602
  ...api,
3340
3603
  ...groupingSupported || query.grouping ? { grouping } : {},
3341
- 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) }
3342
3605
  };
3343
3606
  }
3344
3607
  function nextOrderBy(existing, field, additive) {