@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/README.md +66 -0
- package/dist/index.d.ts +69 -7
- package/dist/index.js +357 -94
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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: [...
|
|
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
|
|
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) => !
|
|
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) =>
|
|
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
|
|
577
|
-
const
|
|
578
|
-
const
|
|
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) => (
|
|
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) => (
|
|
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) => (
|
|
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) => (
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
1593
|
+
(term) => predicatesOf5(term).map((predicate) => predicate.field)
|
|
1537
1594
|
),
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
|
|
2145
|
-
|
|
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
|
-
|
|
2166
|
-
|
|
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
|
|
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) =>
|
|
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 =
|
|
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:
|
|
2345
|
-
const
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
2510
|
-
const
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 ((
|
|
2756
|
+
if ((execution2?.fields[computedFieldName2(definition.id)]?.select || v2Request.orderBy.some(
|
|
2623
2757
|
(term) => term.field === computedFieldName2(definition.id)
|
|
2624
|
-
)) && executionRevision(
|
|
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
|
|
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) => (
|
|
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
|
|
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) => (
|
|
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:
|
|
3310
|
-
|
|
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
|
|
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) {
|