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

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
  }
@@ -321,6 +348,7 @@ import { ITEM_DETAILS_COLUMN, itemPageFields } from "@pythia-software/query-tabl
321
348
 
322
349
  // src/useItemDetails.ts
323
350
  import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
351
+ import { isPropertyFieldLoaded, jsonInspectionScopeKey, MAX_JSON_INSPECTIONS } from "@pythia-software/query-table-core";
324
352
 
325
353
  // src/usePropertyView.ts
326
354
  import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
@@ -509,7 +537,7 @@ function initialDetailModes(config) {
509
537
  const available = ALL_MODES.filter((mode) => !config?.modes || config.modes.includes(mode));
510
538
  return config?.defaultModes ? available.filter((mode) => config.defaultModes.includes(mode)) : available.includes("modal") ? ["modal"] : available.slice(0, 1);
511
539
  }
512
- function useDetailProperties(mode, schema, config, query, setQuery, enabled) {
540
+ function useDetailProperties(mode, schema, config, query, setQuery, enabled, readOnly = false) {
513
541
  const snapshot = query.itemDetails?.views?.[mode] ?? query.itemDetails?.view;
514
542
  const seed = useRef3(snapshot);
515
543
  const properties = usePropertyView({
@@ -539,7 +567,7 @@ function useDetailProperties(mode, schema, config, query, setQuery, enabled) {
539
567
  if (target !== localViewKey) properties.setView(next);
540
568
  } else if (pendingView.current && pendingView.current.query === queryViewKey) {
541
569
  if (pendingView.current.local === localViewKey) pendingView.current = null;
542
- } else if (!properties.loading && localViewKey !== queryViewKey && (queryViewKey !== void 0 || localViewKey !== previous.local)) {
570
+ } else if (!readOnly && !properties.loading && localViewKey !== queryViewKey && (queryViewKey !== void 0 || localViewKey !== previous.local)) {
543
571
  setQuery((current) => ({ ...current, itemDetails: {
544
572
  modes: current.itemDetails?.modes ?? initialDetailModes(config),
545
573
  ...current.itemDetails,
@@ -561,7 +589,9 @@ function useItemDetails({
561
589
  refreshKey,
562
590
  links,
563
591
  queryReady = true,
564
- queryParameter: queryParameter2
592
+ queryParameter: queryParameter2,
593
+ lookupRow,
594
+ subject
565
595
  }) {
566
596
  const enabled = Boolean(options);
567
597
  const config = typeof options === "object" ? options : void 0;
@@ -573,9 +603,10 @@ function useItemDetails({
573
603
  const sharedModal = requested?.mode === "modal" && query.itemDetails?.modes.includes("modal") && (availableModes.includes("hover") || availableModes.includes("page") && !page.urlTemplate.trim());
574
604
  const active = requested && (modes.includes(requested.mode) || sharedModal) ? requested : null;
575
605
  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);
606
+ const readOnly = subject === "group";
607
+ const modalProperties = useDetailProperties("modal", detailSchema, config, query, setQuery, syncProperties, readOnly);
608
+ const hoverProperties = useDetailProperties("hover", detailSchema, config, query, setQuery, syncProperties, readOnly);
609
+ const expandProperties = useDetailProperties("expand", detailSchema, config, query, setQuery, syncProperties, readOnly);
579
610
  const propertiesByMode = { modal: modalProperties, hover: hoverProperties, expand: expandProperties };
580
611
  const views = {
581
612
  modal: modalProperties.view,
@@ -585,10 +616,14 @@ function useItemDetails({
585
616
  const customLoad = config?.loadRow;
586
617
  const customLoadRef = useRef3(customLoad);
587
618
  customLoadRef.current = customLoad;
619
+ const lookupRef = useRef3(lookupRow);
620
+ lookupRef.current = lookupRow;
621
+ const hasLookup = Boolean(lookupRow);
588
622
  const hasCustomLoad = Boolean(customLoad);
589
623
  const sourceType = config?.objectType ?? detailSchema.name;
590
624
  const fallbackRows = !customLoad && !clientRows && !transport?.fetchRow ? rows : void 0;
591
625
  const loadRow = useCallback3(async (id, signal) => {
626
+ if (lookupRef.current) return lookupRef.current(id) ?? null;
592
627
  if (customLoadRef.current) return customLoadRef.current(id, signal);
593
628
  if (clientRows) {
594
629
  const row2 = clientRows.find((row3) => rowId(row3) === id);
@@ -600,10 +635,10 @@ function useItemDetails({
600
635
  }
601
636
  const row = rows.find((row2) => rowId(row2) === id);
602
637
  return row ? { row, loadedFields: Object.keys(row) } : null;
603
- }, [hasCustomLoad, sourceType, clientRows, transport?.fetchRow, fallbackRows, rowId]);
638
+ }, [hasLookup, hasCustomLoad, sourceType, clientRows, transport?.fetchRow, fallbackRows, rowId]);
604
639
  const [retryKey, setRetryKey] = useState3(0);
605
640
  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;
641
+ const activeId = enabled && !lookupRow ? active?.id : void 0;
607
642
  const epoch = `${refreshKey}:${retryKey}`;
608
643
  const currentResult = result.id === activeId && result.source === loadRow && result.epoch === epoch;
609
644
  useEffect3(() => {
@@ -612,7 +647,7 @@ function useItemDetails({
612
647
  setResult((previous) => previous.id === void 0 && !previous.loading && !previous.error && !previous.record ? previous : { source: void 0, epoch: "", id: void 0, record: null, loading: false, error: null });
613
648
  return;
614
649
  }
615
- setResult({ source: loadRow, epoch, id: activeId, record: null, loading: true, error: null });
650
+ setResult((previous) => ({ source: loadRow, epoch, id: activeId, record: previous.id === activeId && previous.source === loadRow ? previous.record : null, loading: true, error: null }));
616
651
  void Promise.resolve().then(() => loadRow(activeId, controller.signal)).then((record) => {
617
652
  if (!controller.signal.aborted) setResult({ source: loadRow, epoch, id: activeId, record, loading: false, error: null });
618
653
  }).catch((error) => {
@@ -627,9 +662,66 @@ function useItemDetails({
627
662
  });
628
663
  return () => controller.abort();
629
664
  }, [activeId, loadRow, refreshKey, retryKey]);
665
+ const preserveFocus = useRef3(false);
666
+ const attentionSequence = useRef3(0);
667
+ const [attentionRequest, setAttentionRequest] = useState3({ revision: 0, pending: false });
668
+ const attentionLatest = useRef3({ loadRow, setQuery, modes, views, active, query });
669
+ attentionLatest.current = { loadRow, setQuery, modes, views, active, query };
670
+ useEffect3(() => {
671
+ const registry = config?.attention;
672
+ if (!enabled || !registry) return;
673
+ const provider = { reveal: async (target, signal) => {
674
+ if (target.rowId === void 0 || signal.aborted) return false;
675
+ const current = attentionLatest.current;
676
+ const record = await current.loadRow(target.rowId, signal);
677
+ if (!record || signal.aborted) return false;
678
+ if (target.field !== void 0 && !detailSchema.fields.some((field) => field.name === target.field && field.property?.enabled !== false && isPropertyFieldLoaded(field, record.loadedFields))) return false;
679
+ const mode = current.active?.id === target.rowId ? current.active.mode : current.modes.includes("expand") ? "expand" : current.modes.includes("modal") ? "modal" : null;
680
+ if (!mode || target.field !== void 0 && !current.views[mode].properties.some((property) => property.field === target.field)) return false;
681
+ preserveFocus.current = true;
682
+ const revision = ++attentionSequence.current;
683
+ setAttentionRequest({ revision, pending: true });
684
+ try {
685
+ current.setQuery((previous) => ({ ...previous, itemDetails: {
686
+ ...previous.itemDetails,
687
+ modes: previous.itemDetails?.modes ?? current.modes,
688
+ views: current.views,
689
+ active: { id: target.rowId, mode }
690
+ } }));
691
+ const mounted = await registry.whenRegistered({ dataset: detailSchema.name, rowId: target.rowId }, signal);
692
+ if (mounted.ready) await mounted.ready(signal);
693
+ return signal.aborted ? false : await mounted.reveal(target, signal);
694
+ } finally {
695
+ setAttentionRequest((previous) => previous.revision === revision ? { revision, pending: false } : previous);
696
+ }
697
+ } };
698
+ return registry.register({ dataset: detailSchema.name }, provider);
699
+ }, [enabled, config?.attention, detailSchema]);
630
700
  if (!enabled) return;
701
+ const looked = lookupRow && active ? lookupRow(active.id) : null;
631
702
  return {
703
+ ...subject ? { subject } : {},
632
704
  ...links ? { links } : {},
705
+ ...config?.attention ? { attention: config.attention } : {},
706
+ preserveFocus: preserveFocus.current,
707
+ attentionRevision: attentionRequest.revision,
708
+ attentionPending: attentionRequest.pending,
709
+ jsonInspections: query.itemDetails?.jsonInspections ?? [],
710
+ setJsonInspection(entry) {
711
+ if (entry.dataset !== detailSchema.name) return;
712
+ setQuery((current) => {
713
+ const inspections = (current.itemDetails?.jsonInspections ?? []).filter((previous) => jsonInspectionScopeKey(previous) !== jsonInspectionScopeKey(entry));
714
+ while (inspections.length >= MAX_JSON_INSPECTIONS) {
715
+ const closed = inspections.findIndex((previous) => !previous.state.open);
716
+ inspections.splice(closed < 0 ? 0 : closed, 1);
717
+ }
718
+ return { ...current, itemDetails: {
719
+ ...current.itemDetails,
720
+ modes: current.itemDetails?.modes ?? modes,
721
+ jsonInspections: [...inspections, entry]
722
+ } };
723
+ });
724
+ },
633
725
  schema: detailSchema,
634
726
  queryParameter: queryParameter2 ?? config?.queryParameter ?? "q",
635
727
  properties: modalProperties,
@@ -639,9 +731,9 @@ function useItemDetails({
639
731
  active,
640
732
  page,
641
733
  pageSchema: schema,
642
- record: currentResult ? result.record : null,
643
- loading: activeId !== void 0 && (!currentResult || result.loading),
644
- error: currentResult ? result.error : null,
734
+ record: lookupRow ? looked ?? null : result.id === activeId && result.source === loadRow ? result.record : null,
735
+ loading: lookupRow ? looked === void 0 : activeId !== void 0 && (!currentResult || result.loading),
736
+ error: lookupRow ? null : currentResult ? result.error : null,
645
737
  ...config?.relationships ? { relationships: config.relationships } : {},
646
738
  loadRow,
647
739
  retry: () => setRetryKey((value) => value + 1),
@@ -654,6 +746,8 @@ function useItemDetails({
654
746
  });
655
747
  },
656
748
  open(id, mode) {
749
+ config?.attention?.cancel();
750
+ preserveFocus.current = false;
657
751
  if (!modes.includes(mode)) return;
658
752
  setQuery((current) => ({ ...current, itemDetails: {
659
753
  ...current.itemDetails,
@@ -663,6 +757,7 @@ function useItemDetails({
663
757
  } }));
664
758
  },
665
759
  close() {
760
+ config?.attention?.cancel();
666
761
  setQuery((current) => {
667
762
  if (!current.itemDetails?.active) return current;
668
763
  const { active: _active, ...state } = current.itemDetails;
@@ -674,6 +769,7 @@ function useItemDetails({
674
769
 
675
770
  // src/computedExecution.ts
676
771
  import {
772
+ predicatesOf as predicatesOf2,
677
773
  isComputedField,
678
774
  readFieldValue
679
775
  } from "@pythia-software/query-table-core";
@@ -689,7 +785,7 @@ function validateComputedResponse(request, result, schema) {
689
785
  const executed = /* @__PURE__ */ new Set([
690
786
  ...request.select.filter(isComputedField),
691
787
  ...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)
788
+ ...request.where.flatMap((term) => predicatesOf2(term).map((predicate) => predicate.field)).filter(isComputedField)
693
789
  ]);
694
790
  for (const [name, revision] of Object.entries(request.expectedRevisions)) {
695
791
  const id = name.replace(/^@computed\//, "");
@@ -724,6 +820,7 @@ function validateComputedResponse(request, result, schema) {
724
820
 
725
821
  // src/previewRows.ts
726
822
  import {
823
+ predicatesOf as predicatesOf3,
727
824
  sourceExecutionSchema,
728
825
  toQueryRequestV3,
729
826
  toServerQuery,
@@ -734,13 +831,13 @@ async function fetchPreviewRows(transport, query, schema, execution, definitions
734
831
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
735
832
  const sourceSchema = sourceExecutionSchema(baseSchema, execution, definitions);
736
833
  let result;
737
- const computedFilter = query.where.some((term) => ("any" in term ? term.any : [term]).some((predicate) => predicate.field.startsWith("@computed/")));
834
+ const computedFilter = query.where.some((term) => predicatesOf3(term).some((predicate) => predicate.field.startsWith("@computed/")));
738
835
  if (transport.fetchRowsV2 && execution && !query.grouping && !computedFilter) {
739
836
  const request = toServerQueryV2(query, sourceSchema, execution);
740
837
  if (request.diagnostics.length)
741
838
  throw Error(request.diagnostics.map((issue) => issue.message).join(" "));
742
839
  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}`))))
840
+ 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
841
  throw Error("Computed definitions changed. Refresh the server execution handshake.");
745
842
  }
746
843
  const response = await transport.fetchRowsV2(request, signal);
@@ -772,6 +869,7 @@ async function fetchPreviewRows(transport, query, schema, execution, definitions
772
869
  // src/useComputedColumns.ts
773
870
  import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
774
871
  import {
872
+ predicatesOf as predicatesOf4,
775
873
  isGroupLinkField,
776
874
  applyQuery,
777
875
  compileFormula,
@@ -1173,7 +1271,7 @@ function useComputedColumns(options) {
1173
1271
  offset: 0
1174
1272
  };
1175
1273
  const now = options.now ?? Date.now();
1176
- const computedFilter = request.where.some((term) => ("any" in term ? term.any : [term]).some((predicate) => isComputedField2(predicate.field)));
1274
+ const computedFilter = request.where.some((term) => predicatesOf4(term).some((predicate) => isComputedField2(predicate.field)));
1177
1275
  const v3 = !(transport.fetchRowsV2 && options.execution && !request.grouping && !computedFilter) && transport.fetchQueryV3 && transport.queryCapabilities?.version === 3;
1178
1276
  const pageSize = Math.min(500, v3 ? transport.queryCapabilities.maxRows : 500);
1179
1277
  if (!Number.isInteger(pageSize) || pageSize < 1) throw Error("Invalid negotiated preview row limit.");
@@ -1262,17 +1360,22 @@ import {
1262
1360
  applyQuery as applyQuery2,
1263
1361
  memoryStorageAdapter,
1264
1362
  normalizeQueryState as normalizeQueryState3,
1363
+ parseHostQueryState,
1265
1364
  readFieldValue as readFieldValue4,
1266
1365
  selectedFields as selectedFields2,
1267
1366
  toServerQuery as toServerQuery2,
1268
1367
  queriesEqual as queriesEqual2,
1269
- isOrGroup as isOrGroup2,
1368
+ isWhereConjunction as isWhereConjunction2,
1369
+ isWhereGroup,
1370
+ predicatesOf as predicatesOf6,
1270
1371
  metricPlans as metricPlans2,
1271
1372
  validateMetric as validateMetric2,
1272
- metricDependencies,
1373
+ metricDependencies as metricDependencies2,
1273
1374
  isComputedField as isComputedField4,
1274
1375
  computedFieldName as computedFieldName2,
1275
- toServerQueryV2 as toServerQueryV22
1376
+ toServerQueryV2 as toServerQueryV22,
1377
+ groupKeyField,
1378
+ groupKeyFields as groupKeyFields2
1276
1379
  } from "@pythia-software/query-table-core";
1277
1380
 
1278
1381
  // src/useRequestActivity.ts
@@ -1385,11 +1488,12 @@ import {
1385
1488
  normalizeQueryState as normalizeQueryState2,
1386
1489
  evaluateMetrics,
1387
1490
  metricPlans,
1491
+ metricDependencies,
1388
1492
  hasRegex as hasRegex2,
1389
1493
  metricPopulationError,
1390
1494
  hasMetricSeries,
1391
1495
  planMetricSeries,
1392
- predicatesOf as predicatesOf3,
1496
+ predicatesOf as predicatesOf5,
1393
1497
  toAggregationQuery,
1394
1498
  toMetricQuery,
1395
1499
  validateMetric,
@@ -1545,11 +1649,10 @@ async function executeMetrics(query, context, signal) {
1545
1649
  shown.flatMap((clause) => [
1546
1650
  ...clause.groupBy,
1547
1651
  ...(clause.where ?? []).flatMap(
1548
- (term) => predicatesOf3(term).map((predicate) => predicate.field)
1652
+ (term) => predicatesOf5(term).map((predicate) => predicate.field)
1549
1653
  ),
1550
- ...metricPlans(clause, context.schema, context.resolveComputed).flatMap(
1551
- (p) => p.dependencies
1552
- )
1654
+ // Includes a measured minimum's field, which the page must also hold.
1655
+ ...metricDependencies(clause, context.schema, context.resolveComputed)
1553
1656
  ])
1554
1657
  );
1555
1658
  for (const name of names) {
@@ -2134,7 +2237,7 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now, snaps
2134
2237
  // src/useQueryLinks.ts
2135
2238
  import { useCallback as useCallback11, useEffect as useEffect9, useRef as useRef8, useState as useState10 } from "react";
2136
2239
  import { encodeQuery } from "@pythia-software/query-table-core";
2137
- function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, onUrlChange) {
2240
+ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, onUrlChange, decorate = (url) => url) {
2138
2241
  const latest = useRef8(query);
2139
2242
  latest.current = query;
2140
2243
  const [kind, setKind] = useState10(null);
@@ -2153,14 +2256,15 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2153
2256
  if (!syncUrl || !ready || typeof window === "undefined") return;
2154
2257
  try {
2155
2258
  const result = service.local(query, window.location.href, parameter);
2156
- window.history.replaceState(window.history.state, "", result.url);
2157
- onUrlChange(result.url);
2259
+ const url = decorate(result.url, query);
2260
+ window.history.replaceState(window.history.state, "", url);
2261
+ onUrlChange(url);
2158
2262
  setKind(result.kind);
2159
2263
  setError(null);
2160
2264
  } catch (e) {
2161
2265
  setError(e instanceof Error ? e : Error(String(e)));
2162
2266
  }
2163
- }, [queryKey, syncUrl, ready, service, parameter, onUrlChange]);
2267
+ }, [queryKey, syncUrl, ready, service, parameter, onUrlChange, decorate]);
2164
2268
  const snapshot = useCallback11(async (q) => {
2165
2269
  const captured = encodeQuery(q);
2166
2270
  const base = typeof window !== "undefined" ? window.location.href : void 0;
@@ -2174,8 +2278,9 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2174
2278
  const url = new URL(base);
2175
2279
  url.searchParams.set(parameter, "@" + saved.code);
2176
2280
  const result = service.local(q, url.toString(), parameter);
2177
- window.history.replaceState(window.history.state, "", result.url);
2178
- onUrlChange(result.url);
2281
+ const next = decorate(result.url, q);
2282
+ window.history.replaceState(window.history.state, "", next);
2283
+ onUrlChange(next);
2179
2284
  setKind("durable");
2180
2285
  }
2181
2286
  return saved;
@@ -2186,7 +2291,7 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2186
2291
  operations.current--;
2187
2292
  if (mounted.current) setBusy(operations.current > 0);
2188
2293
  }
2189
- }, [service, syncUrl, ready, parameter, onUrlChange]);
2294
+ }, [service, syncUrl, ready, parameter, onUrlChange, decorate]);
2190
2295
  const create = useCallback11(async (q = latest.current, options = {}) => {
2191
2296
  const baseUrl = options.baseUrl ?? window.location.href;
2192
2297
  const param = options.parameter ?? parameter;
@@ -2196,7 +2301,8 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2196
2301
  setError(null);
2197
2302
  try {
2198
2303
  if (!ready) throw Error("Resolve or reset the query link before sharing it.");
2199
- const result = await service.durable(q, baseUrl, param);
2304
+ const durable = await service.durable(q, baseUrl, param);
2305
+ const result = param === parameter ? { ...durable, url: decorate(durable.url, q) } : durable;
2200
2306
  if (mounted.current && syncUrl && ready && param === parameter && window.location.href === baseUrl && encodeQuery(latest.current) === captured) {
2201
2307
  window.history.replaceState(window.history.state, "", result.url);
2202
2308
  onUrlChange(result.url);
@@ -2210,8 +2316,11 @@ function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, o
2210
2316
  operations.current--;
2211
2317
  if (mounted.current) setBusy(operations.current > 0);
2212
2318
  }
2213
- }, [service, syncUrl, ready, parameter, onUrlChange]);
2214
- const local = useCallback11((q, base = window.location.href, param = parameter) => service.local(q, base, param), [service, parameter]);
2319
+ }, [service, syncUrl, ready, parameter, onUrlChange, decorate]);
2320
+ const local = useCallback11((q, base = window.location.href, param = parameter) => {
2321
+ const result = service.local(q, base, param);
2322
+ return param === parameter ? { ...result, url: decorate(result.url, q) } : result;
2323
+ }, [service, parameter, decorate]);
2215
2324
  return { maxUrlBytes, kind, busy, error, create, local, snapshot };
2216
2325
  }
2217
2326
 
@@ -2248,7 +2357,7 @@ function defaultsFor(schema, details = false) {
2248
2357
  });
2249
2358
  }
2250
2359
  function cloneWhereTerm(term) {
2251
- const copy = isOrGroup2(term) ? { ...term, any: term.any.map((c) => ({ ...c })) } : { ...term };
2360
+ const copy = isWhereGroup(term) ? { ...term, any: term.any.map((c) => isWhereConjunction2(c) ? { all: c.all.map((m) => ({ ...m })) } : { ...c }) } : { ...term };
2252
2361
  if (term.setFilter)
2253
2362
  copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
2254
2363
  return copy;
@@ -2264,6 +2373,7 @@ function cloneQueryState(q) {
2264
2373
  offset: q.offset
2265
2374
  };
2266
2375
  if (q.fitToScreen) clone.fitToScreen = true;
2376
+ if (q.hostState) clone.hostState = { version: q.hostState.version, params: Object.fromEntries(Object.entries(q.hostState.params).map(([name, values]) => [name, [...values]])) };
2267
2377
  if (q.aggregations)
2268
2378
  clone.aggregations = JSON.parse(
2269
2379
  JSON.stringify(q.aggregations)
@@ -2325,16 +2435,61 @@ function hasQueryLink(url, parameter) {
2325
2435
  function queryParameter(opts) {
2326
2436
  return opts.queryParameter ?? (typeof opts.itemDetails === "object" ? opts.itemDetails.queryParameter : void 0) ?? "q";
2327
2437
  }
2438
+ function withHostState(query, host) {
2439
+ if (!host) {
2440
+ if (query.hostState) throw Error("This query includes host state that this table does not support.");
2441
+ return query;
2442
+ }
2443
+ const raw = query.hostState ?? host.defaults;
2444
+ if (raw.version === 0) throw Error("This query's host state could not be read.");
2445
+ const state = parseHostQueryState(host.normalize ? host.normalize(parseHostQueryState(raw)) : raw);
2446
+ return { ...query, hostState: state };
2447
+ }
2448
+ function withoutHostState(query) {
2449
+ const { hostState: _hostState, ...rest } = query;
2450
+ return rest;
2451
+ }
2452
+ function hostStateFromUrl(location, host) {
2453
+ const url = new URL(location);
2454
+ const params = {};
2455
+ for (const name of host.ownedParameters) {
2456
+ const values = url.searchParams.getAll(name);
2457
+ if (values.length) params[name] = values;
2458
+ }
2459
+ return Object.keys(params).length ? { version: host.defaults.version, params } : void 0;
2460
+ }
2461
+ function writeHostParameters(url, host, state) {
2462
+ const next = new URL(url);
2463
+ for (const name of host.ownedParameters) next.searchParams.delete(name);
2464
+ for (const name of host.ownedParameters)
2465
+ for (const value of state?.params[name] ?? []) next.searchParams.append(name, value);
2466
+ return next.toString();
2467
+ }
2468
+ var HOST_REQUEST_METHODS = /* @__PURE__ */ new Set(["fetchRows", "fetchRowsV2", "fetchQueryV3", "fetchMetrics", "fetchAggregations", "fetchDistinctValues"]);
2469
+ function withHostTransport(transport, hostStateKey) {
2470
+ if (!transport || !hostStateKey) return transport;
2471
+ const hostState = JSON.parse(hostStateKey);
2472
+ const wrappers = /* @__PURE__ */ new Map();
2473
+ return new Proxy(Object.create(transport), {
2474
+ get(_target, key) {
2475
+ const method = Reflect.get(transport, key, transport);
2476
+ if (!HOST_REQUEST_METHODS.has(String(key)) || typeof method !== "function") return method;
2477
+ if (!wrappers.has(key))
2478
+ wrappers.set(key, (request, signal) => method.call(transport, { ...request, hostState }, signal));
2479
+ return wrappers.get(key);
2480
+ }
2481
+ });
2482
+ }
2328
2483
  function resolveInitial(opts) {
2329
- const defaults = defaultsFor(opts.schema, Boolean(opts.itemDetails));
2484
+ const defaults = withHostState(defaultsFor(opts.schema, Boolean(opts.itemDetails)), opts.hostQuery);
2330
2485
  if (opts.initialQuery)
2331
2486
  return {
2332
- q: normalizeQueryState3(opts.initialQuery, defaults),
2487
+ q: withHostState(normalizeQueryState3(opts.initialQuery, defaults), opts.hostQuery),
2333
2488
  fromUrl: false
2334
2489
  };
2335
2490
  if (opts.syncUrl === true && opts.active !== false && typeof window !== "undefined") {
2336
2491
  const parameter = queryParameter(opts);
2337
- if (hasQueryLink(window.location.href, parameter))
2492
+ if (hasQueryLink(window.location.href, parameter) || opts.hostQuery && hostStateFromUrl(window.location.href, opts.hostQuery))
2338
2493
  return {
2339
2494
  q: defaults,
2340
2495
  fromUrl: true
@@ -2353,8 +2508,13 @@ function useQueryTable(opts) {
2353
2508
  const active = opts.active !== false;
2354
2509
  const urlSyncEnabled = opts.syncUrl === true;
2355
2510
  const syncUrl = urlSyncEnabled && active;
2356
- const { transport: metadataTransport, metricTransport, activity: requestActivity } = useRequestActivity(sourceTransport, opts.metricTransport);
2357
- const transport = metadataTransport;
2511
+ const { transport: activityTransport, metricTransport: activityMetricTransport, activity: requestActivity } = useRequestActivity(sourceTransport, opts.metricTransport);
2512
+ const host = opts.hostQuery;
2513
+ const hostRef = useRef9(host);
2514
+ hostRef.current = host;
2515
+ const hostDefaultsKey = host ? JSON.stringify(host.defaults) : "";
2516
+ if (host?.ownedParameters.includes(queryParameter(opts)))
2517
+ throw Error(`hostQuery.ownedParameters cannot include the table's own "${queryParameter(opts)}" parameter.`);
2358
2518
  const storage = useMemo9(
2359
2519
  () => opts.storage ?? memoryStorageAdapter(),
2360
2520
  [opts.storage]
@@ -2367,7 +2527,10 @@ function useQueryTable(opts) {
2367
2527
  const now = opts.now ?? Date.now;
2368
2528
  const nowRef = useRef9(now);
2369
2529
  const onRefreshRef = useRef9(opts.onRefresh);
2370
- const defaults = useMemo9(() => defaultsFor(sourceSchema, Boolean(opts.itemDetails)), [sourceSchema, Boolean(opts.itemDetails)]);
2530
+ const defaults = useMemo9(
2531
+ () => withHostState(defaultsFor(sourceSchema, Boolean(opts.itemDetails)), hostRef.current),
2532
+ [sourceSchema, Boolean(opts.itemDetails), hostDefaultsKey]
2533
+ );
2371
2534
  const initRef = useRef9(resolveInitial(opts));
2372
2535
  const [restoration, setRestoration] = useState11({ loading: initRef.current.fromUrl, error: null });
2373
2536
  const [storageReady, setStorageReady] = useState11(Boolean(initialQuery));
@@ -2404,6 +2567,10 @@ function useQueryTable(opts) {
2404
2567
  () => opts.canonicalizeQuery?.(storedQuery) ?? storedQuery,
2405
2568
  [storedQuery, opts.canonicalizeQuery]
2406
2569
  );
2570
+ const hostStateKey = host && query.hostState ? JSON.stringify(query.hostState) : "";
2571
+ const metadataTransport = useMemo9(() => withHostTransport(activityTransport, hostStateKey), [activityTransport, hostStateKey]);
2572
+ const metricTransport = useMemo9(() => withHostTransport(activityMetricTransport, hostStateKey), [activityMetricTransport, hostStateKey]);
2573
+ const transport = metadataTransport;
2407
2574
  const sourceCatalogue = useMemo9(() => sourceExecutionSchema2(sourceSchema, opts.computedExecution), [sourceSchema, opts.computedExecution]);
2408
2575
  const sourceRows = providedRows;
2409
2576
  const groupingModel = useMemo9(() => createGroupingModel(sourceCatalogue), [sourceCatalogue]);
@@ -2433,6 +2600,7 @@ function useQueryTable(opts) {
2433
2600
  const executionQuery = useMemo9(() => withoutGrouping2(query), [query]);
2434
2601
  const byName = useMemo9(() => new Map(schema.fields.map((field) => [field.name, field])), [schema]);
2435
2602
  const detailsEnabled = Boolean(opts.itemDetails) && !query.grouping;
2603
+ const groupDetailsEnabled = Boolean(opts.itemDetails) && Boolean(query.grouping);
2436
2604
  const validateExecution = useCallback12((q) => {
2437
2605
  if (prepared?.error) throw prepared.error;
2438
2606
  opts.validateQuery?.(query.grouping ? { ...q, grouping: query.grouping } : q);
@@ -2445,6 +2613,7 @@ function useQueryTable(opts) {
2445
2613
  const [loading, setLoading] = useState11(false);
2446
2614
  const [error, setError] = useState11(null);
2447
2615
  const [committedPage, setCommittedPage] = useState11(null);
2616
+ const [failedPage, setFailedPage] = useState11(null);
2448
2617
  const [committedSource, setCommittedSource] = useState11(null);
2449
2618
  const rows = useMemo9(() => committedSource?.schema === schema ? committedRows : [], [committedSource?.schema, schema, committedRows]);
2450
2619
  const [autoRefreshStatus, setAutoRefreshStatus] = useState11(null);
@@ -2464,7 +2633,8 @@ function useQueryTable(opts) {
2464
2633
  updateQueryHistory((history) => {
2465
2634
  const prev = history.query;
2466
2635
  const candidate = typeof next === "function" ? next(prev) : next;
2467
- const nextQuery = normalizeQueryState3(candidate, defaults);
2636
+ const normalized = normalizeQueryState3(candidate, defaults);
2637
+ const nextQuery = withHostState(normalized.hostState || !prev.hostState ? normalized : { ...normalized, hostState: prev.hostState }, hostRef.current);
2468
2638
  if (queriesEqual2(prev, nextQuery)) return history;
2469
2639
  return { query: cloneQueryState(nextQuery), past: [...history.past, cloneQueryState(prev)], future: [] };
2470
2640
  });
@@ -2481,9 +2651,15 @@ function useQueryTable(opts) {
2481
2651
  }, [setQuery]);
2482
2652
  const sourceFilters = useQueryFilters(sourceCatalogue, setSourceQuery);
2483
2653
  const observedQueryUrl = useRef9(null);
2654
+ const tableUrlKey = (url) => {
2655
+ const params = new URL(url).searchParams;
2656
+ return JSON.stringify([params.getAll(parameter), ...(hostRef.current?.ownedParameters ?? []).map((name) => params.getAll(name))]);
2657
+ };
2484
2658
  const recordQueryUrl = useCallback12((url) => {
2485
- observedQueryUrl.current = JSON.stringify(new URL(url).searchParams.getAll(parameter));
2659
+ observedQueryUrl.current = tableUrlKey(url);
2486
2660
  }, [parameter]);
2661
+ const ownedParametersKey = JSON.stringify(host?.ownedParameters ?? null);
2662
+ const decorateLink = useCallback12((url, q) => hostRef.current ? writeHostParameters(url, hostRef.current, q.hostState) : url, [ownedParametersKey]);
2487
2663
  const links = useQueryLinks(
2488
2664
  query,
2489
2665
  linkService,
@@ -2491,7 +2667,8 @@ function useQueryTable(opts) {
2491
2667
  storageReady && !restoration.loading && !restoration.error,
2492
2668
  parameter,
2493
2669
  opts.links?.maxUrlBytes ?? 2e3,
2494
- recordQueryUrl
2670
+ recordQueryUrl,
2671
+ decorateLink
2495
2672
  );
2496
2673
  const restoreSources = useRef9({ storage, linkService, defaults, initialQuery });
2497
2674
  restoreSources.current = { storage, linkService, defaults, initialQuery };
@@ -2513,17 +2690,21 @@ function useQueryTable(opts) {
2513
2690
  signal?.addEventListener("abort", abort2, { once: true });
2514
2691
  if (signal?.aborted) currentController.abort();
2515
2692
  setStorageReady(false);
2516
- const explicitLink = !!location && (navigation || urlSyncEnabled) && hasQueryLink(location, parameter);
2693
+ const host2 = hostRef.current;
2694
+ const hostFromUrl = host2 && location && (navigation || urlSyncEnabled) ? hostStateFromUrl(location, host2) : void 0;
2695
+ const explicitLink = !!location && (navigation || urlSyncEnabled) && (hasQueryLink(location, parameter) || !!hostFromUrl);
2517
2696
  setRestoration({ loading: true, error: null });
2518
2697
  try {
2519
2698
  if (currentController.signal.aborted) throw new DOMException("Query restoration cancelled.", "AbortError");
2520
2699
  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));
2700
+ const linked = fromUrl ?? (hostFromUrl ? defaults2 : null);
2701
+ const defaultSaved = linked ? null : await storage2.loadDefaultSaved?.(sourceSchema.name);
2702
+ let restored = linked ?? defaultSaved?.query ?? (navigation ? defaults2 : await storage2.loadLast(sourceSchema.name));
2523
2703
  if (currentController.signal.aborted) throw new DOMException("Query restoration cancelled.", "AbortError");
2704
+ if (linked && !fromUrl?.hostState && hostFromUrl) restored = { ...linked, hostState: hostFromUrl };
2524
2705
  if (!cancelled && run === generation && epoch === manualQueryEpoch.current) {
2525
2706
  if (restored) {
2526
- const normalized = normalizeQueryState3(restored, defaults2);
2707
+ const normalized = withHostState(normalizeQueryState3(restored, defaults2), host2);
2527
2708
  savedBaseline.current = defaultSaved || fromUrl?.grouping ? { source: sourceSchema.name, query: cloneQueryState(normalized) } : null;
2528
2709
  viewRevision.current++;
2529
2710
  updateQueryHistory(() => ({ query: normalized, past: [], future: [] }));
@@ -2548,7 +2729,7 @@ function useQueryTable(opts) {
2548
2729
  }
2549
2730
  restoreHandler.current = async (location, signal) => {
2550
2731
  location = new URL(location, typeof window !== "undefined" ? window.location.href : void 0).href;
2551
- const key = JSON.stringify(new URL(location).searchParams.getAll(parameter));
2732
+ const key = tableUrlKey(location);
2552
2733
  const current = restorationCurrent.current;
2553
2734
  if (key === observedQueryUrl.current && current.storageReady && !current.restoration.loading && !current.restoration.error) return;
2554
2735
  recordQueryUrl(location);
@@ -2558,7 +2739,7 @@ function useQueryTable(opts) {
2558
2739
  if (typeof window !== "undefined") recordQueryUrl(window.location.href);
2559
2740
  const navigate = () => {
2560
2741
  if (!restorationCurrent.current.active) return;
2561
- const key = JSON.stringify(new URL(window.location.href).searchParams.getAll(parameter));
2742
+ const key = tableUrlKey(window.location.href);
2562
2743
  if (key === observedQueryUrl.current) return;
2563
2744
  observedQueryUrl.current = key;
2564
2745
  void restore(true).catch(() => {
@@ -2618,7 +2799,7 @@ function useQueryTable(opts) {
2618
2799
  const definitionKey = JSON.stringify(computedRevisions);
2619
2800
  const queryKey = useMemo9(() => JSON.stringify(query), [query]);
2620
2801
  const serverRowQuery = useMemo9(() => {
2621
- const execution = opts.computedExecution;
2802
+ const execution2 = opts.computedExecution;
2622
2803
  const detailConfig = typeof opts.itemDetails === "object" ? opts.itemDetails : void 0;
2623
2804
  const page = query.itemDetails?.page ?? detailConfig?.page;
2624
2805
  const pageEnabled = Boolean(opts.itemDetails) && (query.itemDetails?.modes ?? initialDetailModes(detailConfig)).includes("page") && (!detailConfig?.modes || detailConfig.modes.includes("page"));
@@ -2627,13 +2808,13 @@ function useQueryTable(opts) {
2627
2808
  ...query.select.length ? query.select : defaults.select,
2628
2809
  ...pageFields.filter((name) => displaySchema.fields.some((field) => field.name === name)).map((field) => ({ field }))
2629
2810
  ] } : executionQuery;
2630
- const v2Request = execution && transport?.fetchRowsV2 ? toServerQueryV22(requestQuery, displaySchema, execution) : void 0;
2811
+ const v2Request = execution2 && transport?.fetchRowsV2 ? toServerQueryV22(requestQuery, displaySchema, execution2) : void 0;
2631
2812
  const request = v2Request ?? toServerQuery2(requestQuery, displaySchema);
2632
2813
  if (v2Request)
2633
2814
  for (const definition of computed.definitions) {
2634
- if ((execution?.fields[computedFieldName2(definition.id)]?.select || v2Request.orderBy.some(
2815
+ if ((execution2?.fields[computedFieldName2(definition.id)]?.select || v2Request.orderBy.some(
2635
2816
  (term) => term.field === computedFieldName2(definition.id)
2636
- )) && executionRevision(execution, definition.id) !== void 0 && executionRevision(execution, definition.id) !== definition.revision)
2817
+ )) && executionRevision(execution2, definition.id) !== void 0 && executionRevision(execution2, definition.id) !== definition.revision)
2637
2818
  v2Request.diagnostics.push({
2638
2819
  from: 0,
2639
2820
  to: 1,
@@ -2644,7 +2825,7 @@ function useQueryTable(opts) {
2644
2825
  for (const clause of query.aggregations ?? [])
2645
2826
  if (clause.scope === "shownRows") {
2646
2827
  try {
2647
- for (const name of metricDependencies(
2828
+ for (const name of metricDependencies2(
2648
2829
  clause,
2649
2830
  schema,
2650
2831
  resolveComputed
@@ -2694,7 +2875,7 @@ function useQueryTable(opts) {
2694
2875
  ]
2695
2876
  );
2696
2877
  const executionKey = JSON.stringify(opts.computedExecution ?? null);
2697
- const pageContext = `${schema.name}:${rowQueryKey}:${nonce}:${executionKey}`;
2878
+ const pageContext = `${schema.name}:${rowQueryKey}:${nonce}:${executionKey}:${hostStateKey}`;
2698
2879
  useEffect10(() => {
2699
2880
  if (!active || !storageReady || restoration.loading || restoration.error) {
2700
2881
  setRows((previous) => previous.length ? [] : previous);
@@ -2708,10 +2889,11 @@ function useQueryTable(opts) {
2708
2889
  const run = async () => {
2709
2890
  setLoading(true);
2710
2891
  setError(null);
2892
+ setFailedPage(null);
2711
2893
  try {
2712
2894
  validateExecution(query);
2713
2895
  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))) {
2896
+ 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
2897
  const request = toQueryRequestV32(
2716
2898
  { ...query, aggregations: [], select: [
2717
2899
  ...query.select.filter((column) => isComputedField4(column.field)),
@@ -2792,6 +2974,7 @@ function useQueryTable(opts) {
2792
2974
  } catch (e) {
2793
2975
  if (!cancelled && !ac.signal.aborted) {
2794
2976
  setError(e);
2977
+ setFailedPage(pageContext);
2795
2978
  setRows([]);
2796
2979
  setTotal(null);
2797
2980
  setCommittedPage(null);
@@ -2820,11 +3003,13 @@ function useQueryTable(opts) {
2820
3003
  active,
2821
3004
  storageReady
2822
3005
  ]);
3006
+ const persistLast = opts.persistLast ?? "always";
3007
+ const hostStorageUnsupported = Boolean(host) && !storage.storesHostState;
2823
3008
  useEffect10(() => {
2824
- if (!storageReady || restoration.loading || restoration.error) return;
3009
+ if (persistLast !== "always" || !storageReady || restoration.loading || restoration.error || hostStorageUnsupported) return;
2825
3010
  const t = setTimeout(() => void storage.saveLast(sourceSchema.name, query), 400);
2826
3011
  return () => clearTimeout(t);
2827
- }, [queryKey, storage, sourceSchema.name, query, restoration, storageReady]);
3012
+ }, [persistLast, queryKey, storage, sourceSchema.name, query, restoration, storageReady, hostStorageUnsupported]);
2828
3013
  const idField = useMemo9(
2829
3014
  () => schema.fields.find((f) => f.name === schema.idField),
2830
3015
  [schema]
@@ -2845,7 +3030,7 @@ function useQueryTable(opts) {
2845
3030
  useEffect10(() => {
2846
3031
  selection.clear();
2847
3032
  }, [identityScope, selection.clear]);
2848
- const columnSchema = useMemo9(() => detailColumnSchema(displaySchema, detailsEnabled), [displaySchema, detailsEnabled]);
3033
+ const columnSchema = useMemo9(() => detailColumnSchema(displaySchema, detailsEnabled || groupDetailsEnabled), [displaySchema, detailsEnabled, groupDetailsEnabled]);
2849
3034
  const resultDefaults = useMemo9(() => query.grouping ? defaultsFor(schema) : defaults, [Boolean(query.grouping), schema, defaults]);
2850
3035
  const select = useSelect(query, setQuery, columnSchema);
2851
3036
  const columnDrag = useColumnDrag();
@@ -2853,7 +3038,7 @@ function useQueryTable(opts) {
2853
3038
  sourceSchema.name,
2854
3039
  query,
2855
3040
  (q) => {
2856
- const normalized = normalizeQueryState3(q, defaults);
3041
+ const normalized = withHostState(normalizeQueryState3(q, defaults), hostRef.current);
2857
3042
  savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(normalized) };
2858
3043
  viewRevision.current++;
2859
3044
  setQuery(normalized);
@@ -2862,11 +3047,23 @@ function useQueryTable(opts) {
2862
3047
  now,
2863
3048
  links.snapshot
2864
3049
  );
2865
- const saved = { ...savedQueries, save: async (name) => {
3050
+ const hostStorageError = host && !storage.storesHostState ? "This storage adapter cannot save host query state (set StorageAdapter.storesHostState once it keeps QueryState.hostState)." : null;
3051
+ const savedItems = useMemo9(() => !host ? savedQueries.items : savedQueries.items.map((item) => {
3052
+ try {
3053
+ return { ...item, query: withHostState(normalizeQueryState3(item.query, defaults), hostRef.current) };
3054
+ } catch {
3055
+ return item;
3056
+ }
3057
+ }), [savedQueries.items, Boolean(host), defaults]);
3058
+ const saved = { ...savedQueries, items: savedItems, save: async (name) => {
3059
+ if (hostStorageError) throw Error(hostStorageError);
2866
3060
  const item = await savedQueries.save(name);
2867
- savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(normalizeQueryState3(item.query, defaults)) };
3061
+ savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(withHostState(normalizeQueryState3(item.query, defaults), hostRef.current)) };
2868
3062
  viewRevision.current++;
2869
3063
  return item;
3064
+ }, update: async (id) => {
3065
+ if (hostStorageError) throw Error(hostStorageError);
3066
+ return savedQueries.update(id);
2870
3067
  } };
2871
3068
  const pageReady = committedPage === pageContext && committedSource?.schema === schema && committedSource?.transport === transport && committedSource?.clientRows === clientRows && !error;
2872
3069
  const dataToken = useRef9(0);
@@ -2886,7 +3083,7 @@ function useQueryTable(opts) {
2886
3083
  const capabilities = metricTransport.queryCapabilities;
2887
3084
  const refused = /* @__PURE__ */ new Map();
2888
3085
  for (const clause of metricQuery.aggregations ?? []) {
2889
- const issue = metricPopulationError2(clause, { filters: capabilities.metricFilters, minCount: capabilities.metricMinCount });
3086
+ const issue = metricPopulationError2(clause, { filters: capabilities.metricFilters, minCount: capabilities.metricMinCount, minCountMeasures: capabilities.metricMinCountMeasures });
2890
3087
  if (issue) refused.set(clause.id, issue);
2891
3088
  }
2892
3089
  const runnable = (metricQuery.aggregations ?? []).filter((clause) => !refused.has(clause.id));
@@ -2938,7 +3135,7 @@ function useQueryTable(opts) {
2938
3135
  nonce,
2939
3136
  validateExecution,
2940
3137
  {
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 } : {},
3138
+ ...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
3139
  ...groupingNow !== void 0 ? { now: groupingNow, strictLocalMetrics: true } : {},
2943
3140
  shownRows: rows,
2944
3141
  rowsReady: metricRowsReady,
@@ -3239,6 +3436,62 @@ function useQueryTable(opts) {
3239
3436
  query.where
3240
3437
  ]
3241
3438
  );
3439
+ const executionIdentity = JSON.stringify([pageContext, query.aggregations ?? [], query.where, groupKey, nonce]);
3440
+ const executionGeneration = useRef9({ key: "", generation: 0 });
3441
+ if (executionGeneration.current.key !== executionIdentity)
3442
+ executionGeneration.current = { key: executionIdentity, generation: executionGeneration.current.generation + 1 };
3443
+ const execution = useMemo9(() => {
3444
+ if (!active || !storageReady || restoration.loading || restoration.error) return null;
3445
+ const rowsStatus = pageReady ? "succeeded" : error && failedPage === pageContext ? "failed" : "running";
3446
+ const metricError = aggState.error ?? aggState.results?.metrics.find((entry) => entry.error)?.error;
3447
+ const metricsStatus = !query.aggregations?.length ? "none" : aggState.loading || !aggState.results && !aggState.error ? "running" : metricError ? "failed" : "succeeded";
3448
+ const status = rowsStatus === "failed" || metricsStatus === "failed" ? "failed" : rowsStatus === "succeeded" && metricsStatus !== "running" ? "succeeded" : "running";
3449
+ return {
3450
+ query,
3451
+ key: executionIdentity,
3452
+ generation: executionGeneration.current.generation,
3453
+ status,
3454
+ rows: rowsStatus,
3455
+ metrics: metricsStatus,
3456
+ error: rowsStatus === "failed" ? error : metricError ? metricError instanceof Error ? metricError : Error(String(metricError)) : null
3457
+ };
3458
+ }, [
3459
+ active,
3460
+ storageReady,
3461
+ restoration.loading,
3462
+ restoration.error,
3463
+ pageReady,
3464
+ error,
3465
+ failedPage,
3466
+ pageContext,
3467
+ aggState.error,
3468
+ aggState.results,
3469
+ aggState.loading,
3470
+ query,
3471
+ executionIdentity
3472
+ ]);
3473
+ const succeededQuery = execution?.status === "succeeded" ? execution.query : null;
3474
+ const pendingLast = useRef9(null);
3475
+ const savedLast = useRef9(null);
3476
+ useEffect10(() => {
3477
+ if (persistLast !== "succeeded" || !succeededQuery || hostStorageUnsupported) return;
3478
+ const key = JSON.stringify([sourceSchema.name, succeededQuery]);
3479
+ if (pendingLast.current?.key === key || !pendingLast.current && savedLast.current === key) return;
3480
+ if (pendingLast.current) clearTimeout(pendingLast.current.timer);
3481
+ const save = () => {
3482
+ pendingLast.current = null;
3483
+ savedLast.current = key;
3484
+ void storage.saveLast(sourceSchema.name, succeededQuery);
3485
+ };
3486
+ pendingLast.current = { key, save, timer: setTimeout(save, 400) };
3487
+ }, [persistLast, succeededQuery, storage, sourceSchema.name, hostStorageUnsupported]);
3488
+ useEffect10(() => () => {
3489
+ const pending = pendingLast.current;
3490
+ if (pending) {
3491
+ clearTimeout(pending.timer);
3492
+ pending.save();
3493
+ }
3494
+ }, []);
3242
3495
  const sourceRowId = useCallback12((row) => {
3243
3496
  const field = sourceSchema.fields.find((f) => f.name === sourceSchema.idField);
3244
3497
  const value = field ? readFieldValue4(field, row) : row[sourceSchema.idField];
@@ -3258,16 +3511,71 @@ function useQueryTable(opts) {
3258
3511
  queryParameter: parameter,
3259
3512
  queryReady: storageReady && !restoration.loading && !restoration.error
3260
3513
  });
3514
+ const groupPopulation = query.grouping ? JSON.stringify([query.grouping, query.where, nonce, hostStateKey]) : "";
3515
+ const groupRecords = useRef9({ population: "", source: [], rows: /* @__PURE__ */ new Map() });
3516
+ const groupSource = [clientRows, transport, schema];
3517
+ if (groupRecords.current.population !== groupPopulation || groupSource.some((part, i) => part !== groupRecords.current.source[i]))
3518
+ groupRecords.current = { population: groupPopulation, source: groupSource, rows: /* @__PURE__ */ new Map() };
3519
+ const groupsSettled = groupDetailsEnabled && pageReady && !restoration.loading;
3520
+ if (groupsSettled) for (const row of rows) {
3521
+ const id = rowId(row);
3522
+ if (id != null) groupRecords.current.rows.set(id, row);
3523
+ }
3524
+ const groupDetailOptions = useMemo9(() => {
3525
+ if (!groupDetailsEnabled) return void 0;
3526
+ const config = typeof opts.itemDetails === "object" ? opts.itemDetails : void 0;
3527
+ const modes = (config?.modes ?? ["hover", "modal", "expand"]).filter((mode) => mode !== "page");
3528
+ return {
3529
+ modes,
3530
+ defaultModes: initialDetailModes(config).filter((mode) => modes.includes(mode)),
3531
+ objectType: `${config?.objectType ?? sourceSchema.name}:groups`,
3532
+ ...config?.storage ? { storage: config.storage } : {}
3533
+ };
3534
+ }, [groupDetailsEnabled, opts.itemDetails, sourceSchema.name]);
3535
+ const groupDetailSchema = useMemo9(() => ({
3536
+ ...displaySchema,
3537
+ defaultSelect: displaySchema.fields.filter((field) => field.name !== displaySchema.idField).map((field) => ({ field: field.name })),
3538
+ fields: displaySchema.fields.map((field) => field.name === displaySchema.idField ? { ...field, property: { enabled: false } } : field)
3539
+ }), [displaySchema]);
3540
+ const setGroupQuery = useCallback12((next) => setQuery((current) => current.grouping ? typeof next === "function" ? next(current) : next : current), [setQuery]);
3541
+ const groupDetails = useItemDetails({
3542
+ schema: groupDetailSchema,
3543
+ query,
3544
+ setQuery: setGroupQuery,
3545
+ options: groupDetailOptions,
3546
+ rows,
3547
+ clientRows: void 0,
3548
+ rowId,
3549
+ transport: void 0,
3550
+ refreshKey: nonce,
3551
+ links,
3552
+ queryParameter: parameter,
3553
+ queryReady: storageReady && !restoration.loading && !restoration.error,
3554
+ lookupRow: (id) => {
3555
+ const row = groupRecords.current.rows.get(id);
3556
+ return row ? { row } : groupsSettled || error ? null : void 0;
3557
+ },
3558
+ subject: "group"
3559
+ });
3261
3560
  const api = {
3262
3561
  restoration: {
3263
3562
  status: restoration.error ? "error" : !storageReady || restoration.loading ? "resolving" : "ready",
3264
3563
  ready: active && storageReady && !restoration.loading && !restoration.error,
3265
3564
  error: restoration.error
3266
3565
  },
3566
+ execution,
3567
+ ...host && query.hostState ? {
3568
+ definition: { query: withoutHostState(query), hostState: query.hostState },
3569
+ setDefinition: (next) => {
3570
+ const current = queryHistoryRef.current.query;
3571
+ const value = typeof next === "function" ? next({ query: withoutHostState(current), hostState: current.hostState }) : next;
3572
+ setQuery(withHostState({ ...normalizeQueryState3(value.query, defaults), hostState: value.hostState }, hostRef.current));
3573
+ }
3574
+ } : {},
3267
3575
  restoreLocation,
3268
3576
  links,
3269
3577
  schema: columnSchema,
3270
- ...itemDetails && detailsEnabled ? { itemDetails } : {},
3578
+ ...itemDetails && detailsEnabled ? { itemDetails } : groupDetails ? { itemDetails: groupDetails } : {},
3271
3579
  query,
3272
3580
  defaults: resultDefaults,
3273
3581
  setQuery,
@@ -3310,6 +3618,25 @@ function useQueryTable(opts) {
3310
3618
  visibleFields,
3311
3619
  rowId
3312
3620
  };
3621
+ const sourceFilterValues = useCallback12(async (name, search) => {
3622
+ if (metadataTransport?.fetchDistinctValues) return metadataTransport.fetchDistinctValues({ field: name, search });
3623
+ const field = groupingModel.sourceField(name);
3624
+ return field && sourceRows ? { ...distinctValuesFromRows(field, sourceRows, search), hasNull: fieldHasNullInRows(field, sourceRows) } : { values: [], hasMore: false };
3625
+ }, [metadataTransport, groupingModel, sourceRows]);
3626
+ const sourceFieldStats = useCallback12(async (names, signal) => {
3627
+ if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
3628
+ const requested = [...new Set(names)];
3629
+ if (metadataTransport?.fetchFieldStats) {
3630
+ const backendFields = requested.filter(
3631
+ (name) => sourceCatalogue.fields.find((field) => field.name === name)?.source.kind === "backend"
3632
+ );
3633
+ return backendFields.length ? metadataTransport.fetchFieldStats(backendFields, signal) : {};
3634
+ }
3635
+ return Object.fromEntries(requested.flatMap((name) => {
3636
+ const field = groupingModel.sourceField(name);
3637
+ return field && sourceRows ? [[name, { distinct: new Set(sourceRows.map((row) => JSON.stringify(readFieldValue4(field, row)))).size }]] : [];
3638
+ }));
3639
+ }, [metadataTransport, groupingModel, sourceRows, sourceCatalogue]);
3313
3640
  const sourceApi = {
3314
3641
  ...sourceFilters,
3315
3642
  loading,
@@ -3318,25 +3645,8 @@ function useQueryTable(opts) {
3318
3645
  setQuery: setSourceQuery,
3319
3646
  ...itemDetails ? { itemDetails } : {},
3320
3647
  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
- }
3648
+ filterValues: sourceFilterValues,
3649
+ fieldStats: sourceFieldStats
3340
3650
  };
3341
3651
  const grouping = useGroupingController(
3342
3652
  api,
@@ -3350,7 +3660,7 @@ function useQueryTable(opts) {
3350
3660
  return {
3351
3661
  ...api,
3352
3662
  ...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) }
3663
+ 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
3664
  };
3355
3665
  }
3356
3666
  function nextOrderBy(existing, field, additive) {