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