@pythia-software/query-table-react 0.2.0 → 0.4.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 +23 -0
- package/dist/index.d.ts +51 -3
- package/dist/index.js +697 -121
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,32 +1,459 @@
|
|
|
1
|
+
// src/useComputedColumns.ts
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
applyQuery,
|
|
5
|
+
compileFormula,
|
|
6
|
+
computedFieldName,
|
|
7
|
+
groupPreview,
|
|
8
|
+
isComputedField,
|
|
9
|
+
memoryComputedColumnStore,
|
|
10
|
+
readFieldValue,
|
|
11
|
+
toServerQuery,
|
|
12
|
+
validateComputedColumn,
|
|
13
|
+
isSelectable
|
|
14
|
+
} from "@pythia-software/query-table-core";
|
|
15
|
+
|
|
16
|
+
// src/formulaWorker.ts
|
|
17
|
+
import {
|
|
18
|
+
formulaRuntime
|
|
19
|
+
} from "@pythia-software/query-table-core";
|
|
20
|
+
var createFormulaWorker = () => {
|
|
21
|
+
const code = `const evaluate = ${formulaRuntime.toString()}; self.onmessage = e => { const { ast, inputs } = e.data; self.postMessage(inputs.map(row => evaluate(ast, row))); };`;
|
|
22
|
+
const url = URL.createObjectURL(
|
|
23
|
+
new Blob([code], { type: "text/javascript" })
|
|
24
|
+
);
|
|
25
|
+
try {
|
|
26
|
+
return new Worker(url);
|
|
27
|
+
} finally {
|
|
28
|
+
URL.revokeObjectURL(url);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function evaluateFormulaRows(ast, inputs, signal, factory = createFormulaWorker) {
|
|
32
|
+
if (signal?.aborted)
|
|
33
|
+
return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
34
|
+
if (!inputs.length) return Promise.resolve([]);
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let worker;
|
|
37
|
+
try {
|
|
38
|
+
worker = factory();
|
|
39
|
+
} catch (e) {
|
|
40
|
+
reject(
|
|
41
|
+
new Error(
|
|
42
|
+
`Formula worker unavailable: ${e instanceof Error ? e.message : String(e)}`
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let timer;
|
|
48
|
+
const started = Date.now();
|
|
49
|
+
let offset = 0;
|
|
50
|
+
const results = [];
|
|
51
|
+
const cleanup = () => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
worker.terminate();
|
|
54
|
+
signal?.removeEventListener("abort", abort);
|
|
55
|
+
};
|
|
56
|
+
const abort = () => {
|
|
57
|
+
cleanup();
|
|
58
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
59
|
+
};
|
|
60
|
+
const send = () => {
|
|
61
|
+
if (Date.now() - started > 3e4) {
|
|
62
|
+
cleanup();
|
|
63
|
+
reject(
|
|
64
|
+
new Error(
|
|
65
|
+
"Formula exceeded the 30-second processing budget. Reduce the row count or simplify the formula."
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
timer = setTimeout(() => {
|
|
71
|
+
cleanup();
|
|
72
|
+
reject(
|
|
73
|
+
new Error(
|
|
74
|
+
"Formula timed out. Check the regex or reduce the row count."
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
}, 2e3);
|
|
78
|
+
try {
|
|
79
|
+
worker.postMessage({ ast, inputs: inputs.slice(offset, offset + 200) });
|
|
80
|
+
} catch (e) {
|
|
81
|
+
cleanup();
|
|
82
|
+
reject(e);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
86
|
+
worker.onerror = () => {
|
|
87
|
+
cleanup();
|
|
88
|
+
reject(new Error("Formula worker failed."));
|
|
89
|
+
};
|
|
90
|
+
worker.onmessage = (event) => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
results.push(...event.data);
|
|
93
|
+
offset += event.data.length;
|
|
94
|
+
if (offset >= inputs.length) {
|
|
95
|
+
cleanup();
|
|
96
|
+
resolve(results);
|
|
97
|
+
} else send();
|
|
98
|
+
};
|
|
99
|
+
send();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/useComputedColumns.ts
|
|
104
|
+
function useComputedColumns(options) {
|
|
105
|
+
const { schema, query, rows, transport, clientRows, workerFactory } = options;
|
|
106
|
+
const store = useMemo(
|
|
107
|
+
() => options.store ?? memoryComputedColumnStore(),
|
|
108
|
+
[options.store]
|
|
109
|
+
);
|
|
110
|
+
const [catalogue, setCatalogue] = useState(null);
|
|
111
|
+
const definitions = useMemo(
|
|
112
|
+
() => catalogue?.dataset === schema.name && catalogue.store === store ? catalogue.items : [],
|
|
113
|
+
[catalogue, schema.name, store]
|
|
114
|
+
);
|
|
115
|
+
const [loading, setLoading] = useState(false), [error, setError] = useState(null);
|
|
116
|
+
const catalogueRequest = useRef(null);
|
|
117
|
+
const reload = useCallback(async () => {
|
|
118
|
+
catalogueRequest.current?.abort();
|
|
119
|
+
const ac = new AbortController();
|
|
120
|
+
catalogueRequest.current = ac;
|
|
121
|
+
setLoading(true);
|
|
122
|
+
setError(null);
|
|
123
|
+
try {
|
|
124
|
+
const items = await store.list(schema.name, ac.signal);
|
|
125
|
+
if (ac.signal.aborted) return;
|
|
126
|
+
const valid = items.map(validateComputedColumn).sort((a, b) => a.id.localeCompare(b.id));
|
|
127
|
+
if (new Set(valid.map((c) => c.id)).size !== valid.length)
|
|
128
|
+
throw new Error("Duplicate computed column IDs.");
|
|
129
|
+
setCatalogue(
|
|
130
|
+
(previous) => previous?.dataset === schema.name && previous.store === store && JSON.stringify(previous.items) === JSON.stringify(valid) ? previous : { dataset: schema.name, store, items: valid }
|
|
131
|
+
);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (!ac.signal.aborted)
|
|
134
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
135
|
+
} finally {
|
|
136
|
+
if (!ac.signal.aborted) setLoading(false);
|
|
137
|
+
}
|
|
138
|
+
}, [store, schema.name]);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
void reload();
|
|
141
|
+
return () => catalogueRequest.current?.abort();
|
|
142
|
+
}, [reload]);
|
|
143
|
+
useEffect(
|
|
144
|
+
() => store.subscribe?.(schema.name, () => {
|
|
145
|
+
void reload();
|
|
146
|
+
}),
|
|
147
|
+
[store, schema.name, reload]
|
|
148
|
+
);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const focus = () => {
|
|
151
|
+
void reload();
|
|
152
|
+
};
|
|
153
|
+
window.addEventListener("focus", focus);
|
|
154
|
+
return () => window.removeEventListener("focus", focus);
|
|
155
|
+
}, [reload]);
|
|
156
|
+
const inputFields = useMemo(
|
|
157
|
+
() => schema.fields.filter(isSelectable).filter(
|
|
158
|
+
(f) => f.source.kind === "backend" || f.source.accessor && f.source.dependencies
|
|
159
|
+
),
|
|
160
|
+
[schema]
|
|
161
|
+
);
|
|
162
|
+
const compile = useCallback(
|
|
163
|
+
(source, editingId) => {
|
|
164
|
+
const stack = new Set(editingId ? [editingId] : []);
|
|
165
|
+
let expanded = 0;
|
|
166
|
+
const resolve = (name) => {
|
|
167
|
+
if (!isComputedField(name)) return void 0;
|
|
168
|
+
const id = name.slice("@computed/".length), def = definitions.find((d) => d.id === id);
|
|
169
|
+
if (!def) throw new Error(`Computed column unavailable: ${id}`);
|
|
170
|
+
if (stack.has(id))
|
|
171
|
+
throw new Error(`Circular computed-column reference: ${def.label}`);
|
|
172
|
+
if (stack.size >= 20 || ++expanded > 100)
|
|
173
|
+
throw new Error("Computed dependency graph is too large.");
|
|
174
|
+
stack.add(id);
|
|
175
|
+
try {
|
|
176
|
+
return compileFormula(def.expression.source, inputFields, resolve);
|
|
177
|
+
} finally {
|
|
178
|
+
stack.delete(id);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
return compileFormula(source, inputFields, resolve);
|
|
182
|
+
},
|
|
183
|
+
[inputFields, definitions]
|
|
184
|
+
);
|
|
185
|
+
const plans = useMemo(
|
|
186
|
+
() => new Map(
|
|
187
|
+
definitions.map((def) => {
|
|
188
|
+
try {
|
|
189
|
+
return [
|
|
190
|
+
def.id,
|
|
191
|
+
{ plan: compile(def.expression.source, def.id) }
|
|
192
|
+
];
|
|
193
|
+
} catch (e) {
|
|
194
|
+
return [
|
|
195
|
+
def.id,
|
|
196
|
+
{ error: e instanceof Error ? e.message : String(e) }
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
),
|
|
201
|
+
[definitions, compile]
|
|
202
|
+
);
|
|
203
|
+
const backendDependencies = useCallback(
|
|
204
|
+
(plan) => [
|
|
205
|
+
...new Set(
|
|
206
|
+
plan.dependencies.flatMap((name) => {
|
|
207
|
+
const f = inputFields.find((f2) => f2.name === name);
|
|
208
|
+
return f?.source.kind === "backend" ? [name] : f?.source.dependencies ?? [];
|
|
209
|
+
})
|
|
210
|
+
)
|
|
211
|
+
].filter(
|
|
212
|
+
(name) => schema.fields.some(
|
|
213
|
+
(f) => f.name === name && f.source.kind === "backend"
|
|
214
|
+
)
|
|
215
|
+
).sort(),
|
|
216
|
+
[schema, inputFields]
|
|
217
|
+
);
|
|
218
|
+
const inputsFor = useCallback(
|
|
219
|
+
(plan, batch) => {
|
|
220
|
+
let size = 0;
|
|
221
|
+
return batch.map((row) => {
|
|
222
|
+
const values = /* @__PURE__ */ Object.create(null);
|
|
223
|
+
for (const name of plan.dependencies) {
|
|
224
|
+
const field = inputFields.find((f) => f.name === name);
|
|
225
|
+
const raw = readFieldValue(field, row);
|
|
226
|
+
values[name] = raw instanceof Date ? raw.toISOString() : raw;
|
|
227
|
+
}
|
|
228
|
+
size += JSON.stringify(values).length;
|
|
229
|
+
if (size > 16e6)
|
|
230
|
+
throw new Error(
|
|
231
|
+
"Input sample exceeds 16 MB. Reduce the rows to process."
|
|
232
|
+
);
|
|
233
|
+
return values;
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
[inputFields]
|
|
237
|
+
);
|
|
238
|
+
const activeSelect = query.select.length ? query.select : schema.defaultSelect ?? [];
|
|
239
|
+
const activeIds = activeSelect.filter((c) => isComputedField(c.field)).map((c) => c.field.slice("@computed/".length)).sort().join(",");
|
|
240
|
+
const [evaluated, setEvaluated] = useState(null);
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
const ac = new AbortController();
|
|
243
|
+
const values = /* @__PURE__ */ new Map();
|
|
244
|
+
void (async () => {
|
|
245
|
+
for (const id of activeIds.split(",").filter(Boolean)) {
|
|
246
|
+
const entry = plans.get(id);
|
|
247
|
+
if (!entry?.plan) continue;
|
|
248
|
+
try {
|
|
249
|
+
values.set(
|
|
250
|
+
id,
|
|
251
|
+
await evaluateFormulaRows(
|
|
252
|
+
entry.plan.ast,
|
|
253
|
+
inputsFor(entry.plan, rows),
|
|
254
|
+
ac.signal,
|
|
255
|
+
workerFactory
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
if (ac.signal.aborted) return;
|
|
260
|
+
values.set(
|
|
261
|
+
id,
|
|
262
|
+
rows.map(() => ({
|
|
263
|
+
value: null,
|
|
264
|
+
error: e instanceof Error ? e.message : String(e)
|
|
265
|
+
}))
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!ac.signal.aborted) setEvaluated({ rows, plans, values });
|
|
270
|
+
})();
|
|
271
|
+
return () => ac.abort();
|
|
272
|
+
}, [activeIds, rows, plans, inputsFor, workerFactory]);
|
|
273
|
+
const displaySchema = useMemo(() => {
|
|
274
|
+
const indices = new Map(rows.map((row, i) => [row, i]));
|
|
275
|
+
const fields = definitions.map((def) => {
|
|
276
|
+
const entry = plans.get(def.id);
|
|
277
|
+
return {
|
|
278
|
+
name: computedFieldName(def.id),
|
|
279
|
+
label: def.label,
|
|
280
|
+
type: entry?.plan?.type === "null" ? "text" : entry?.plan?.type ?? "text",
|
|
281
|
+
group: "Computed columns",
|
|
282
|
+
source: {
|
|
283
|
+
kind: "derived",
|
|
284
|
+
computedId: def.id,
|
|
285
|
+
dependencies: entry?.plan ? backendDependencies(entry.plan) : [],
|
|
286
|
+
accessor: (row) => {
|
|
287
|
+
if (entry?.error) return { computedError: entry.error };
|
|
288
|
+
const result = evaluated?.rows === rows && evaluated.plans === plans ? evaluated.values.get(def.id)?.[indices.get(row) ?? -1] : void 0;
|
|
289
|
+
return result?.error ? { computedError: result.error } : result ? result.value : { computedError: "Calculating\u2026" };
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
filter: { enabled: false },
|
|
293
|
+
sort: { enabled: false },
|
|
294
|
+
aggregate: { measure: false, groupable: false }
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
for (const col of activeSelect)
|
|
298
|
+
if (isComputedField(col.field) && !fields.some((f) => f.name === col.field))
|
|
299
|
+
fields.push({
|
|
300
|
+
name: col.field,
|
|
301
|
+
label: col.field.slice(10),
|
|
302
|
+
type: "text",
|
|
303
|
+
source: {
|
|
304
|
+
kind: "derived",
|
|
305
|
+
computedId: col.field.slice(10),
|
|
306
|
+
accessor: () => ({
|
|
307
|
+
computedError: loading ? "Loading definition\u2026" : "Definition unavailable. Reload the catalogue or contact its owner."
|
|
308
|
+
})
|
|
309
|
+
},
|
|
310
|
+
filter: { enabled: false },
|
|
311
|
+
sort: { enabled: false },
|
|
312
|
+
aggregate: { measure: false, groupable: false }
|
|
313
|
+
});
|
|
314
|
+
return { ...schema, fields: [...schema.fields, ...fields] };
|
|
315
|
+
}, [
|
|
316
|
+
schema,
|
|
317
|
+
definitions,
|
|
318
|
+
plans,
|
|
319
|
+
backendDependencies,
|
|
320
|
+
evaluated,
|
|
321
|
+
rows,
|
|
322
|
+
query.select,
|
|
323
|
+
schema.defaultSelect,
|
|
324
|
+
loading
|
|
325
|
+
]);
|
|
326
|
+
const save = useCallback(
|
|
327
|
+
async (column, expectedRevision) => {
|
|
328
|
+
compile(column.expression.source, column.id);
|
|
329
|
+
const saved = validateComputedColumn(
|
|
330
|
+
await store.save(schema.name, column, expectedRevision)
|
|
331
|
+
);
|
|
332
|
+
setCatalogue((prev) => ({
|
|
333
|
+
dataset: schema.name,
|
|
334
|
+
store,
|
|
335
|
+
items: [
|
|
336
|
+
...prev?.dataset === schema.name && prev.store === store ? prev.items.filter((d) => d.id !== saved.id) : [],
|
|
337
|
+
saved
|
|
338
|
+
]
|
|
339
|
+
}));
|
|
340
|
+
return saved;
|
|
341
|
+
},
|
|
342
|
+
[compile, store, schema.name]
|
|
343
|
+
);
|
|
344
|
+
const preview = useCallback(
|
|
345
|
+
async (source, count, signal, editingId, includeInputs = true) => {
|
|
346
|
+
if (!Number.isInteger(count) || count < 1 || count > 1e4)
|
|
347
|
+
throw new Error("Choose between 1 and 10,000 rows.");
|
|
348
|
+
const plan = compile(source, editingId);
|
|
349
|
+
let batch = [], total = 0;
|
|
350
|
+
if (transport) {
|
|
351
|
+
const request = toServerQuery(
|
|
352
|
+
{
|
|
353
|
+
...query,
|
|
354
|
+
select: [
|
|
355
|
+
.../* @__PURE__ */ new Set([schema.idField, ...backendDependencies(plan)])
|
|
356
|
+
].map((field) => ({ field })),
|
|
357
|
+
limit: Math.min(count, 500),
|
|
358
|
+
offset: 0
|
|
359
|
+
},
|
|
360
|
+
schema
|
|
361
|
+
);
|
|
362
|
+
while (batch.length < count) {
|
|
363
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
364
|
+
const res = await transport.fetchRows(
|
|
365
|
+
{
|
|
366
|
+
...request,
|
|
367
|
+
offset: batch.length,
|
|
368
|
+
limit: Math.min(500, count - batch.length)
|
|
369
|
+
},
|
|
370
|
+
signal
|
|
371
|
+
);
|
|
372
|
+
total = res.total;
|
|
373
|
+
batch.push(...res.rows.slice(0, count - batch.length));
|
|
374
|
+
if (!res.rows.length || batch.length >= total) break;
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
const result = applyQuery(
|
|
378
|
+
clientRows ?? [],
|
|
379
|
+
{ ...query, limit: count, offset: 0 },
|
|
380
|
+
schema
|
|
381
|
+
);
|
|
382
|
+
batch = result.rows;
|
|
383
|
+
total = result.total;
|
|
384
|
+
}
|
|
385
|
+
const inputs = inputsFor(plan, batch), results = await evaluateFormulaRows(
|
|
386
|
+
plan.ast,
|
|
387
|
+
inputs,
|
|
388
|
+
signal,
|
|
389
|
+
workerFactory
|
|
390
|
+
);
|
|
391
|
+
return groupPreview(
|
|
392
|
+
includeInputs ? plan.dependencies : [],
|
|
393
|
+
inputs,
|
|
394
|
+
results,
|
|
395
|
+
total
|
|
396
|
+
);
|
|
397
|
+
},
|
|
398
|
+
[
|
|
399
|
+
compile,
|
|
400
|
+
transport,
|
|
401
|
+
query,
|
|
402
|
+
schema,
|
|
403
|
+
backendDependencies,
|
|
404
|
+
clientRows,
|
|
405
|
+
inputsFor,
|
|
406
|
+
workerFactory
|
|
407
|
+
]
|
|
408
|
+
);
|
|
409
|
+
return {
|
|
410
|
+
displaySchema,
|
|
411
|
+
api: {
|
|
412
|
+
definitions,
|
|
413
|
+
catalogue: displaySchema.fields,
|
|
414
|
+
loading,
|
|
415
|
+
error,
|
|
416
|
+
reload,
|
|
417
|
+
save,
|
|
418
|
+
compile,
|
|
419
|
+
preview
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
1
424
|
// src/useQueryTable.ts
|
|
2
|
-
import { useCallback as
|
|
425
|
+
import { useCallback as useCallback6, useEffect as useEffect4, useMemo as useMemo6, useRef as useRef3, useState as useState6 } from "react";
|
|
3
426
|
import {
|
|
4
427
|
EMPTY_QUERY,
|
|
5
|
-
applyQuery,
|
|
428
|
+
applyQuery as applyQuery2,
|
|
6
429
|
decodeQuery,
|
|
7
430
|
encodeQuery,
|
|
8
431
|
memoryStorageAdapter,
|
|
9
432
|
normalizeQueryState,
|
|
10
|
-
readFieldValue,
|
|
433
|
+
readFieldValue as readFieldValue2,
|
|
11
434
|
selectedFields as selectedFields2,
|
|
12
|
-
toServerQuery,
|
|
13
|
-
queriesEqual
|
|
435
|
+
toServerQuery as toServerQuery2,
|
|
436
|
+
queriesEqual,
|
|
437
|
+
isOrGroup,
|
|
438
|
+
predicatesOf,
|
|
439
|
+
negateClause,
|
|
440
|
+
opsForField
|
|
14
441
|
} from "@pythia-software/query-table-core";
|
|
15
442
|
|
|
16
443
|
// src/useAggregations.ts
|
|
17
|
-
import { useEffect, useMemo, useState } from "react";
|
|
444
|
+
import { useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
|
|
18
445
|
import { applyAggregations, toAggregationQuery } from "@pythia-software/query-table-core";
|
|
19
446
|
var EMPTY_RESULT = { metrics: [] };
|
|
20
|
-
function useAggregations(query, schema, transport, clientRows, debounceMs, nonce) {
|
|
21
|
-
const [results, setResults] =
|
|
22
|
-
const [loading, setLoading] =
|
|
23
|
-
const [error, setError] =
|
|
24
|
-
const key =
|
|
447
|
+
function useAggregations(query, schema, transport, clientRows, debounceMs, nonce, validateQuery) {
|
|
448
|
+
const [results, setResults] = useState2(null);
|
|
449
|
+
const [loading, setLoading] = useState2(false);
|
|
450
|
+
const [error, setError] = useState2(null);
|
|
451
|
+
const key = useMemo2(
|
|
25
452
|
() => JSON.stringify({ where: query.where, aggregations: query.aggregations ?? [] }),
|
|
26
453
|
[query.where, query.aggregations]
|
|
27
454
|
);
|
|
28
455
|
const hasAggregations = (query.aggregations?.length ?? 0) > 0;
|
|
29
|
-
|
|
456
|
+
useEffect2(() => {
|
|
30
457
|
if (!hasAggregations) {
|
|
31
458
|
setResults(null);
|
|
32
459
|
setError(null);
|
|
@@ -39,6 +466,7 @@ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce
|
|
|
39
466
|
setLoading(true);
|
|
40
467
|
setError(null);
|
|
41
468
|
try {
|
|
469
|
+
validateQuery?.(query);
|
|
42
470
|
let res;
|
|
43
471
|
if (transport?.fetchAggregations) {
|
|
44
472
|
res = await transport.fetchAggregations(toAggregationQuery(query, schema), ac.signal);
|
|
@@ -49,7 +477,10 @@ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce
|
|
|
49
477
|
}
|
|
50
478
|
if (!cancelled) setResults(res);
|
|
51
479
|
} catch (e) {
|
|
52
|
-
if (!cancelled && !ac.signal.aborted)
|
|
480
|
+
if (!cancelled && !ac.signal.aborted) {
|
|
481
|
+
setError(e);
|
|
482
|
+
setResults(null);
|
|
483
|
+
}
|
|
53
484
|
} finally {
|
|
54
485
|
if (!cancelled) setLoading(false);
|
|
55
486
|
}
|
|
@@ -60,16 +491,16 @@ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce
|
|
|
60
491
|
clearTimeout(t);
|
|
61
492
|
ac.abort();
|
|
62
493
|
};
|
|
63
|
-
}, [key, hasAggregations, transport, clientRows, schema, debounceMs, nonce]);
|
|
494
|
+
}, [key, hasAggregations, transport, clientRows, schema, debounceMs, nonce, validateQuery]);
|
|
64
495
|
return { results, loading, error };
|
|
65
496
|
}
|
|
66
497
|
|
|
67
498
|
// src/useSelection.ts
|
|
68
|
-
import { useCallback, useMemo as
|
|
499
|
+
import { useCallback as useCallback2, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react";
|
|
69
500
|
function useSelection(displayedIds) {
|
|
70
|
-
const [selected, setSelected] =
|
|
71
|
-
const anchor =
|
|
72
|
-
const toggle =
|
|
501
|
+
const [selected, setSelected] = useState3(() => /* @__PURE__ */ new Set());
|
|
502
|
+
const anchor = useRef2(null);
|
|
503
|
+
const toggle = useCallback2(
|
|
73
504
|
(id, shiftKey = false) => {
|
|
74
505
|
setSelected((prev) => {
|
|
75
506
|
const next = new Set(prev);
|
|
@@ -97,7 +528,7 @@ function useSelection(displayedIds) {
|
|
|
97
528
|
},
|
|
98
529
|
[displayedIds]
|
|
99
530
|
);
|
|
100
|
-
const setPage =
|
|
531
|
+
const setPage = useCallback2((ids, select) => {
|
|
101
532
|
setSelected((prev) => {
|
|
102
533
|
const next = new Set(prev);
|
|
103
534
|
for (const id of ids) {
|
|
@@ -107,7 +538,21 @@ function useSelection(displayedIds) {
|
|
|
107
538
|
return next;
|
|
108
539
|
});
|
|
109
540
|
}, []);
|
|
110
|
-
const
|
|
541
|
+
const replace = useCallback2((ids) => {
|
|
542
|
+
const next = new Set(ids);
|
|
543
|
+
anchor.current = null;
|
|
544
|
+
setSelected(next);
|
|
545
|
+
}, []);
|
|
546
|
+
const retain = useCallback2((ids) => {
|
|
547
|
+
const allowed = new Set(ids);
|
|
548
|
+
anchor.current = null;
|
|
549
|
+
setSelected((prev) => {
|
|
550
|
+
const next = /* @__PURE__ */ new Set();
|
|
551
|
+
for (const id of prev) if (allowed.has(id)) next.add(id);
|
|
552
|
+
return next;
|
|
553
|
+
});
|
|
554
|
+
}, []);
|
|
555
|
+
const pageState = useCallback2(
|
|
111
556
|
(pageIds) => {
|
|
112
557
|
if (pageIds.length === 0) return "none";
|
|
113
558
|
let n = 0;
|
|
@@ -116,35 +561,37 @@ function useSelection(displayedIds) {
|
|
|
116
561
|
},
|
|
117
562
|
[selected]
|
|
118
563
|
);
|
|
119
|
-
const clear =
|
|
564
|
+
const clear = useCallback2(() => {
|
|
120
565
|
anchor.current = null;
|
|
121
566
|
setSelected(/* @__PURE__ */ new Set());
|
|
122
567
|
}, []);
|
|
123
|
-
return
|
|
568
|
+
return useMemo3(
|
|
124
569
|
() => ({
|
|
125
570
|
selected,
|
|
126
571
|
isSelected: (id) => selected.has(id),
|
|
127
572
|
toggle,
|
|
128
573
|
setPage,
|
|
574
|
+
replace,
|
|
575
|
+
retain,
|
|
129
576
|
pageState,
|
|
130
577
|
clear,
|
|
131
578
|
count: selected.size
|
|
132
579
|
}),
|
|
133
|
-
[selected, toggle, setPage, pageState, clear]
|
|
580
|
+
[selected, toggle, setPage, replace, retain, pageState, clear]
|
|
134
581
|
);
|
|
135
582
|
}
|
|
136
583
|
|
|
137
584
|
// src/useSelect.ts
|
|
138
|
-
import { useCallback as
|
|
585
|
+
import { useCallback as useCallback3, useMemo as useMemo4 } from "react";
|
|
139
586
|
import { selectedFields } from "@pythia-software/query-table-core";
|
|
140
587
|
function useSelect(query, setQuery, schema) {
|
|
141
|
-
const fields =
|
|
142
|
-
const visible =
|
|
143
|
-
const hidden =
|
|
588
|
+
const fields = useMemo4(() => selectedFields(schema, query), [schema, query]);
|
|
589
|
+
const visible = useMemo4(() => fields.map((f) => columnFor(query.select, f.name)), [fields, query.select]);
|
|
590
|
+
const hidden = useMemo4(() => {
|
|
144
591
|
const shown = new Set(fields.map((f) => f.name));
|
|
145
592
|
return schema.fields.filter((f) => !shown.has(f.name) && (f.select?.enabled ?? true));
|
|
146
593
|
}, [schema, fields]);
|
|
147
|
-
const writeSelect =
|
|
594
|
+
const writeSelect = useCallback3(
|
|
148
595
|
(mutate) => {
|
|
149
596
|
setQuery((prev) => {
|
|
150
597
|
const current = prev.select.length ? prev.select : visible;
|
|
@@ -153,9 +600,9 @@ function useSelect(query, setQuery, schema) {
|
|
|
153
600
|
},
|
|
154
601
|
[setQuery, visible]
|
|
155
602
|
);
|
|
156
|
-
const show =
|
|
157
|
-
const hide =
|
|
158
|
-
const move =
|
|
603
|
+
const show = useCallback3((field) => writeSelect((c) => c.some((x) => x.field === field) ? c : [...c, { field }]), [writeSelect]);
|
|
604
|
+
const hide = useCallback3((field) => writeSelect((c) => c.filter((x) => x.field !== field)), [writeSelect]);
|
|
605
|
+
const move = useCallback3(
|
|
159
606
|
(field, toIndex) => writeSelect((c) => {
|
|
160
607
|
const from = c.findIndex((x) => x.field === field);
|
|
161
608
|
if (from === -1) return c;
|
|
@@ -165,11 +612,11 @@ function useSelect(query, setQuery, schema) {
|
|
|
165
612
|
}),
|
|
166
613
|
[writeSelect]
|
|
167
614
|
);
|
|
168
|
-
const setWidth =
|
|
615
|
+
const setWidth = useCallback3(
|
|
169
616
|
(field, width) => writeSelect((c) => c.map((x) => x.field === field ? { ...x, width: Math.round(width) } : x)),
|
|
170
617
|
[writeSelect]
|
|
171
618
|
);
|
|
172
|
-
const reset =
|
|
619
|
+
const reset = useCallback3(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);
|
|
173
620
|
return { visible, fields, hidden, show, hide, move, setWidth, reset };
|
|
174
621
|
}
|
|
175
622
|
function columnFor(select, name) {
|
|
@@ -178,11 +625,11 @@ function columnFor(select, name) {
|
|
|
178
625
|
}
|
|
179
626
|
|
|
180
627
|
// src/useColumnDrag.ts
|
|
181
|
-
import { useCallback as
|
|
628
|
+
import { useCallback as useCallback4, useMemo as useMemo5, useState as useState4 } from "react";
|
|
182
629
|
function useColumnDrag() {
|
|
183
|
-
const [state, setState] =
|
|
184
|
-
const start =
|
|
185
|
-
const over =
|
|
630
|
+
const [state, setState] = useState4(null);
|
|
631
|
+
const start = useCallback4((field, index) => setState({ source: field, overIndex: index }), []);
|
|
632
|
+
const over = useCallback4(
|
|
186
633
|
(index) => (
|
|
187
634
|
// Keep the SAME object reference when unchanged so React can bail out of the
|
|
188
635
|
// re-render — onDragOver fires continuously while the cursor moves.
|
|
@@ -190,8 +637,8 @@ function useColumnDrag() {
|
|
|
190
637
|
),
|
|
191
638
|
[]
|
|
192
639
|
);
|
|
193
|
-
const end =
|
|
194
|
-
const preview =
|
|
640
|
+
const end = useCallback4(() => setState(null), []);
|
|
641
|
+
const preview = useCallback4(
|
|
195
642
|
(order) => {
|
|
196
643
|
if (!state) return order;
|
|
197
644
|
const without = order.filter((n) => n !== state.source);
|
|
@@ -201,7 +648,7 @@ function useColumnDrag() {
|
|
|
201
648
|
},
|
|
202
649
|
[state]
|
|
203
650
|
);
|
|
204
|
-
return
|
|
651
|
+
return useMemo5(
|
|
205
652
|
() => ({
|
|
206
653
|
source: state?.source ?? null,
|
|
207
654
|
overIndex: state?.overIndex ?? null,
|
|
@@ -216,12 +663,12 @@ function useColumnDrag() {
|
|
|
216
663
|
}
|
|
217
664
|
|
|
218
665
|
// src/useSavedQueries.ts
|
|
219
|
-
import { useCallback as
|
|
666
|
+
import { useCallback as useCallback5, useEffect as useEffect3, useState as useState5 } from "react";
|
|
220
667
|
function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
|
|
221
|
-
const [items, setItems] =
|
|
222
|
-
const [defaultId, setDefaultId] =
|
|
223
|
-
const [loading, setLoading] =
|
|
224
|
-
const refresh =
|
|
668
|
+
const [items, setItems] = useState5([]);
|
|
669
|
+
const [defaultId, setDefaultId] = useState5(null);
|
|
670
|
+
const [loading, setLoading] = useState5(true);
|
|
671
|
+
const refresh = useCallback5(async () => {
|
|
225
672
|
setLoading(true);
|
|
226
673
|
try {
|
|
227
674
|
const [savedItems, defaultSaved] = await Promise.all([
|
|
@@ -234,10 +681,10 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
|
|
|
234
681
|
setLoading(false);
|
|
235
682
|
}
|
|
236
683
|
}, [storage, key]);
|
|
237
|
-
|
|
684
|
+
useEffect3(() => {
|
|
238
685
|
void refresh();
|
|
239
686
|
}, [refresh]);
|
|
240
|
-
const save =
|
|
687
|
+
const save = useCallback5(
|
|
241
688
|
async (name) => {
|
|
242
689
|
const saved = await storage.saveNamed(key, name, currentQuery, now());
|
|
243
690
|
await refresh();
|
|
@@ -245,25 +692,25 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
|
|
|
245
692
|
},
|
|
246
693
|
[storage, key, currentQuery, now, refresh]
|
|
247
694
|
);
|
|
248
|
-
const load =
|
|
695
|
+
const load = useCallback5(
|
|
249
696
|
(id) => {
|
|
250
697
|
const found = items.find((q) => q.id === id);
|
|
251
698
|
if (found) applyQueryState(found.query);
|
|
252
699
|
},
|
|
253
700
|
[items, applyQueryState]
|
|
254
701
|
);
|
|
255
|
-
const setDefault =
|
|
702
|
+
const setDefault = useCallback5(
|
|
256
703
|
async (id) => {
|
|
257
704
|
await storage.setDefaultSaved?.(key, id);
|
|
258
705
|
await refresh();
|
|
259
706
|
},
|
|
260
707
|
[storage, key, refresh]
|
|
261
708
|
);
|
|
262
|
-
const clearDefault =
|
|
709
|
+
const clearDefault = useCallback5(async () => {
|
|
263
710
|
await storage.setDefaultSaved?.(key, null);
|
|
264
711
|
await refresh();
|
|
265
712
|
}, [storage, key, refresh]);
|
|
266
|
-
const remove =
|
|
713
|
+
const remove = useCallback5(
|
|
267
714
|
async (id) => {
|
|
268
715
|
if (id === defaultId) await storage.setDefaultSaved?.(key, null);
|
|
269
716
|
await storage.deleteSaved(key, id);
|
|
@@ -286,11 +733,14 @@ function defaultsFor(schema) {
|
|
|
286
733
|
offset: 0
|
|
287
734
|
});
|
|
288
735
|
}
|
|
736
|
+
function cloneWhereTerm(term) {
|
|
737
|
+
return isOrGroup(term) ? { any: term.any.map((c) => ({ ...c })) } : { ...term };
|
|
738
|
+
}
|
|
289
739
|
function cloneQueryState(q) {
|
|
290
740
|
const clone = {
|
|
291
741
|
select: q.select.map((column) => ({ ...column })),
|
|
292
|
-
where: q.where.map(
|
|
293
|
-
orderBy: q.orderBy.map((term) =>
|
|
742
|
+
where: q.where.map(cloneWhereTerm),
|
|
743
|
+
orderBy: q.orderBy.map((term) => term.extract ? { ...term, extract: { ...term.extract } } : { ...term }),
|
|
294
744
|
limit: q.limit,
|
|
295
745
|
offset: q.offset
|
|
296
746
|
};
|
|
@@ -321,7 +771,7 @@ function asDistinctValue(value) {
|
|
|
321
771
|
}
|
|
322
772
|
function fieldHasNullInRows(field, rows) {
|
|
323
773
|
for (const row of rows) {
|
|
324
|
-
if (isNullLike(
|
|
774
|
+
if (isNullLike(readFieldValue2(field, row))) return true;
|
|
325
775
|
}
|
|
326
776
|
return false;
|
|
327
777
|
}
|
|
@@ -331,7 +781,7 @@ function distinctValuesFromRows(field, rows, search) {
|
|
|
331
781
|
const seen = /* @__PURE__ */ new Set();
|
|
332
782
|
let hasMore = false;
|
|
333
783
|
for (const row of rows) {
|
|
334
|
-
const raw = asDistinctValue(
|
|
784
|
+
const raw = asDistinctValue(readFieldValue2(field, row));
|
|
335
785
|
if (raw == null || seen.has(raw)) continue;
|
|
336
786
|
if (target && !raw.toLowerCase().includes(target)) continue;
|
|
337
787
|
if (values.length < AUTOCOMPLETE_LIMIT) {
|
|
@@ -355,30 +805,31 @@ function resolveInitial(opts) {
|
|
|
355
805
|
}
|
|
356
806
|
function useQueryTable(opts) {
|
|
357
807
|
const { schema, transport, clientRows, initialQuery, syncUrl = false, debounceMs = 200 } = opts;
|
|
358
|
-
const storage =
|
|
808
|
+
const storage = useMemo6(() => opts.storage ?? memoryStorageAdapter(), [opts.storage]);
|
|
359
809
|
const now = opts.now ?? Date.now;
|
|
360
|
-
const nowRef =
|
|
361
|
-
const onRefreshRef =
|
|
362
|
-
const defaults =
|
|
363
|
-
const byName =
|
|
364
|
-
const initRef =
|
|
365
|
-
const aggIdRef =
|
|
366
|
-
const [
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
const [
|
|
371
|
-
const [
|
|
372
|
-
const [
|
|
373
|
-
const [
|
|
374
|
-
const [
|
|
375
|
-
|
|
810
|
+
const nowRef = useRef3(now);
|
|
811
|
+
const onRefreshRef = useRef3(opts.onRefresh);
|
|
812
|
+
const defaults = useMemo6(() => defaultsFor(schema), [schema]);
|
|
813
|
+
const byName = useMemo6(() => new Map(schema.fields.map((f) => [f.name, f])), [schema]);
|
|
814
|
+
const initRef = useRef3(resolveInitial(opts));
|
|
815
|
+
const aggIdRef = useRef3(0);
|
|
816
|
+
const [storedQuery, setQueryState] = useState6(initRef.current.q);
|
|
817
|
+
const query = useMemo6(() => opts.canonicalizeQuery?.(storedQuery) ?? storedQuery, [storedQuery, opts.canonicalizeQuery]);
|
|
818
|
+
const undoStack = useRef3([cloneQueryState(initRef.current.q)]);
|
|
819
|
+
const redoStack = useRef3([]);
|
|
820
|
+
const [rows, setRows] = useState6([]);
|
|
821
|
+
const [total, setTotal] = useState6(null);
|
|
822
|
+
const [loading, setLoading] = useState6(false);
|
|
823
|
+
const [error, setError] = useState6(null);
|
|
824
|
+
const [nonce, setNonce] = useState6(0);
|
|
825
|
+
const [autoRefreshStatus, setAutoRefreshStatus] = useState6(null);
|
|
826
|
+
useEffect4(() => {
|
|
376
827
|
nowRef.current = now;
|
|
377
828
|
}, [now]);
|
|
378
|
-
|
|
829
|
+
useEffect4(() => {
|
|
379
830
|
onRefreshRef.current = opts.onRefresh;
|
|
380
831
|
}, [opts.onRefresh]);
|
|
381
|
-
const setQuery =
|
|
832
|
+
const setQuery = useCallback6((next) => {
|
|
382
833
|
setQueryState((prev) => {
|
|
383
834
|
const candidate = typeof next === "function" ? next(prev) : next;
|
|
384
835
|
const nextQuery = normalizeQueryState(candidate, defaults);
|
|
@@ -388,7 +839,7 @@ function useQueryTable(opts) {
|
|
|
388
839
|
return cloneQueryState(nextQuery);
|
|
389
840
|
});
|
|
390
841
|
}, [defaults]);
|
|
391
|
-
|
|
842
|
+
useEffect4(() => {
|
|
392
843
|
if (initRef.current.fromUrl || initialQuery) return;
|
|
393
844
|
let cancelled = false;
|
|
394
845
|
void (async () => {
|
|
@@ -405,19 +856,29 @@ function useQueryTable(opts) {
|
|
|
405
856
|
cancelled = true;
|
|
406
857
|
};
|
|
407
858
|
}, []);
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
859
|
+
const { api: computed, displaySchema } = useComputedColumns({
|
|
860
|
+
schema,
|
|
861
|
+
query,
|
|
862
|
+
rows,
|
|
863
|
+
...opts.computedColumnStore ? { store: opts.computedColumnStore } : {},
|
|
864
|
+
...opts.formulaWorkerFactory ? { workerFactory: opts.formulaWorkerFactory } : {},
|
|
865
|
+
...transport ? { transport } : {},
|
|
866
|
+
...clientRows ? { clientRows } : {}
|
|
867
|
+
});
|
|
868
|
+
const queryKey = useMemo6(() => encodeQuery(query), [query]);
|
|
869
|
+
const serverRowQuery = useMemo6(() => toServerQuery2(query, displaySchema), [queryKey, displaySchema]);
|
|
870
|
+
const rowQueryKey = useMemo6(
|
|
411
871
|
() => transport ? `server:${JSON.stringify(serverRowQuery)}` : `client:${JSON.stringify([query.where, query.orderBy, query.limit, query.offset])}`,
|
|
412
872
|
[transport, serverRowQuery, query.where, query.orderBy, query.limit, query.offset]
|
|
413
873
|
);
|
|
414
|
-
|
|
874
|
+
useEffect4(() => {
|
|
415
875
|
const ac = new AbortController();
|
|
416
876
|
let cancelled = false;
|
|
417
877
|
const run = async () => {
|
|
418
878
|
setLoading(true);
|
|
419
879
|
setError(null);
|
|
420
880
|
try {
|
|
881
|
+
opts.validateQuery?.(query);
|
|
421
882
|
if (transport) {
|
|
422
883
|
const res = await transport.fetchRows(serverRowQuery, ac.signal);
|
|
423
884
|
if (!cancelled) {
|
|
@@ -425,14 +886,18 @@ function useQueryTable(opts) {
|
|
|
425
886
|
setTotal(res.total);
|
|
426
887
|
}
|
|
427
888
|
} else {
|
|
428
|
-
const res =
|
|
889
|
+
const res = applyQuery2(clientRows ?? [], query, schema);
|
|
429
890
|
if (!cancelled) {
|
|
430
891
|
setRows(res.rows);
|
|
431
892
|
setTotal(res.total);
|
|
432
893
|
}
|
|
433
894
|
}
|
|
434
895
|
} catch (e) {
|
|
435
|
-
if (!cancelled && !ac.signal.aborted)
|
|
896
|
+
if (!cancelled && !ac.signal.aborted) {
|
|
897
|
+
setError(e);
|
|
898
|
+
setRows([]);
|
|
899
|
+
setTotal(null);
|
|
900
|
+
}
|
|
436
901
|
} finally {
|
|
437
902
|
if (!cancelled) setLoading(false);
|
|
438
903
|
}
|
|
@@ -443,8 +908,8 @@ function useQueryTable(opts) {
|
|
|
443
908
|
clearTimeout(t);
|
|
444
909
|
ac.abort();
|
|
445
910
|
};
|
|
446
|
-
}, [rowQueryKey, transport, clientRows, schema, debounceMs, nonce]);
|
|
447
|
-
|
|
911
|
+
}, [rowQueryKey, transport, clientRows, schema, debounceMs, nonce, opts.validateQuery]);
|
|
912
|
+
useEffect4(() => {
|
|
448
913
|
if (!syncUrl || typeof window === "undefined") return;
|
|
449
914
|
const url = new URL(window.location.href);
|
|
450
915
|
const token = encodeQuery(query);
|
|
@@ -452,18 +917,18 @@ function useQueryTable(opts) {
|
|
|
452
917
|
else url.searchParams.delete("q");
|
|
453
918
|
window.history.replaceState(null, "", url.toString());
|
|
454
919
|
}, [queryKey, syncUrl, query]);
|
|
455
|
-
|
|
920
|
+
useEffect4(() => {
|
|
456
921
|
const t = setTimeout(() => void storage.saveLast(schema.name, query), 400);
|
|
457
922
|
return () => clearTimeout(t);
|
|
458
923
|
}, [queryKey, storage, schema.name, query]);
|
|
459
|
-
const idField =
|
|
460
|
-
const rowId =
|
|
461
|
-
(row) => idField ?
|
|
924
|
+
const idField = useMemo6(() => schema.fields.find((f) => f.name === schema.idField), [schema]);
|
|
925
|
+
const rowId = useCallback6(
|
|
926
|
+
(row) => idField ? readFieldValue2(idField, row) : null,
|
|
462
927
|
[idField]
|
|
463
928
|
);
|
|
464
|
-
const displayedIds =
|
|
929
|
+
const displayedIds = useMemo6(() => rows.map(rowId).filter((id) => id != null), [rows, rowId]);
|
|
465
930
|
const selection = useSelection(displayedIds);
|
|
466
|
-
const select = useSelect(query, setQuery,
|
|
931
|
+
const select = useSelect(query, setQuery, displaySchema);
|
|
467
932
|
const columnDrag = useColumnDrag();
|
|
468
933
|
const saved = useSavedQueries(
|
|
469
934
|
schema.name,
|
|
@@ -472,10 +937,10 @@ function useQueryTable(opts) {
|
|
|
472
937
|
storage,
|
|
473
938
|
now
|
|
474
939
|
);
|
|
475
|
-
const aggState = useAggregations(query, schema, transport, clientRows, debounceMs, nonce);
|
|
940
|
+
const aggState = useAggregations(query, schema, transport, clientRows, debounceMs, nonce, opts.validateQuery);
|
|
476
941
|
const canUndo = undoStack.current.length > 0;
|
|
477
942
|
const canRedo = redoStack.current.length > 0;
|
|
478
|
-
const undo =
|
|
943
|
+
const undo = useCallback6(() => {
|
|
479
944
|
setQueryState((prev) => {
|
|
480
945
|
const previous = undoStack.current.pop();
|
|
481
946
|
if (!previous) return prev;
|
|
@@ -483,7 +948,7 @@ function useQueryTable(opts) {
|
|
|
483
948
|
return cloneQueryState(previous);
|
|
484
949
|
});
|
|
485
950
|
}, []);
|
|
486
|
-
const redo =
|
|
951
|
+
const redo = useCallback6(() => {
|
|
487
952
|
setQueryState((prev) => {
|
|
488
953
|
const next = redoStack.current.pop();
|
|
489
954
|
if (!next) return prev;
|
|
@@ -491,19 +956,95 @@ function useQueryTable(opts) {
|
|
|
491
956
|
return cloneQueryState(next);
|
|
492
957
|
});
|
|
493
958
|
}, []);
|
|
494
|
-
const patch =
|
|
495
|
-
const addFilter =
|
|
496
|
-
const updateFilter =
|
|
497
|
-
(index, clause) => setQuery((q) => ({ ...q, offset: 0, where: q.where.map((
|
|
959
|
+
const patch = useCallback6((p) => setQuery((prev) => ({ ...prev, ...p })), [setQuery]);
|
|
960
|
+
const addFilter = useCallback6((clause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })), [setQuery]);
|
|
961
|
+
const updateFilter = useCallback6(
|
|
962
|
+
(index, clause) => setQuery((q) => ({ ...q, offset: 0, where: q.where.map((t, i) => i === index ? clause : t) })),
|
|
498
963
|
[setQuery]
|
|
499
964
|
);
|
|
500
|
-
const removeFilter =
|
|
965
|
+
const removeFilter = useCallback6(
|
|
501
966
|
(index) => setQuery((q) => ({ ...q, offset: 0, where: q.where.filter((_, i) => i !== index) })),
|
|
502
967
|
[setQuery]
|
|
503
968
|
);
|
|
504
|
-
const clearFilters =
|
|
505
|
-
const
|
|
506
|
-
|
|
969
|
+
const clearFilters = useCallback6(() => setQuery((q) => ({ ...q, offset: 0, where: [] })), [setQuery]);
|
|
970
|
+
const updatePredicate = useCallback6(
|
|
971
|
+
(termIndex, predIndex, clause) => setQuery((q) => ({
|
|
972
|
+
...q,
|
|
973
|
+
offset: 0,
|
|
974
|
+
where: q.where.map((term, i) => {
|
|
975
|
+
if (i !== termIndex) return term;
|
|
976
|
+
if (isOrGroup(term)) return { any: term.any.map((c, j) => j === predIndex ? clause : c) };
|
|
977
|
+
return clause;
|
|
978
|
+
})
|
|
979
|
+
})),
|
|
980
|
+
[setQuery]
|
|
981
|
+
);
|
|
982
|
+
const removePredicate = useCallback6(
|
|
983
|
+
(termIndex, predIndex) => setQuery((q) => {
|
|
984
|
+
const where = [];
|
|
985
|
+
q.where.forEach((term, i) => {
|
|
986
|
+
if (i !== termIndex) {
|
|
987
|
+
where.push(term);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (isOrGroup(term)) {
|
|
991
|
+
const members = term.any.filter((_, j) => j !== predIndex);
|
|
992
|
+
if (members.length === 1) where.push(members[0]);
|
|
993
|
+
else if (members.length > 1) where.push({ any: members });
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
return { ...q, offset: 0, where };
|
|
998
|
+
}),
|
|
999
|
+
[setQuery]
|
|
1000
|
+
);
|
|
1001
|
+
const negateOne = useCallback6(
|
|
1002
|
+
(clause) => {
|
|
1003
|
+
const field = byName.get(clause.field);
|
|
1004
|
+
return negateClause(clause, field ? opsForField(field) : void 0);
|
|
1005
|
+
},
|
|
1006
|
+
[byName]
|
|
1007
|
+
);
|
|
1008
|
+
const negatePredicate = useCallback6(
|
|
1009
|
+
(termIndex, predIndex) => setQuery((q) => ({
|
|
1010
|
+
...q,
|
|
1011
|
+
offset: 0,
|
|
1012
|
+
where: q.where.map((term, i) => {
|
|
1013
|
+
if (i !== termIndex) return term;
|
|
1014
|
+
if (isOrGroup(term)) return { any: term.any.map((c, j) => j === predIndex ? negateOne(c) : c) };
|
|
1015
|
+
return negateOne(term);
|
|
1016
|
+
})
|
|
1017
|
+
})),
|
|
1018
|
+
[setQuery, negateOne]
|
|
1019
|
+
);
|
|
1020
|
+
const reorderFilters = useCallback6(
|
|
1021
|
+
(fromIndex, toIndex) => setQuery((q) => {
|
|
1022
|
+
if (fromIndex < 0 || fromIndex >= q.where.length) return q;
|
|
1023
|
+
const where = [...q.where];
|
|
1024
|
+
const [moved] = where.splice(fromIndex, 1);
|
|
1025
|
+
where.splice(Math.max(0, Math.min(toIndex, where.length)), 0, moved);
|
|
1026
|
+
return { ...q, offset: 0, where };
|
|
1027
|
+
}),
|
|
1028
|
+
[setQuery]
|
|
1029
|
+
);
|
|
1030
|
+
const mergeFilters = useCallback6(
|
|
1031
|
+
(sourceIndex, targetIndex) => setQuery((q) => {
|
|
1032
|
+
if (sourceIndex === targetIndex) return q;
|
|
1033
|
+
const source = q.where[sourceIndex];
|
|
1034
|
+
const target = q.where[targetIndex];
|
|
1035
|
+
if (!source || !target) return q;
|
|
1036
|
+
const merged = { any: [...predicatesOf(target), ...predicatesOf(source)] };
|
|
1037
|
+
const where = [];
|
|
1038
|
+
q.where.forEach((term, i) => {
|
|
1039
|
+
if (i === sourceIndex) return;
|
|
1040
|
+
where.push(i === targetIndex ? merged : term);
|
|
1041
|
+
});
|
|
1042
|
+
return { ...q, offset: 0, where };
|
|
1043
|
+
}),
|
|
1044
|
+
[setQuery]
|
|
1045
|
+
);
|
|
1046
|
+
const resetAll = useCallback6(() => setQuery(() => cloneQueryState(defaults)), [setQuery, defaults]);
|
|
1047
|
+
const addAggregation = useCallback6(
|
|
507
1048
|
(partial) => {
|
|
508
1049
|
const id = `a${nowRef.current()}-${aggIdRef.current++}`;
|
|
509
1050
|
const clause = { id, op: "count", groupBy: [], ...partial };
|
|
@@ -512,14 +1053,14 @@ function useQueryTable(opts) {
|
|
|
512
1053
|
},
|
|
513
1054
|
[setQuery]
|
|
514
1055
|
);
|
|
515
|
-
const updateAggregation =
|
|
1056
|
+
const updateAggregation = useCallback6(
|
|
516
1057
|
(id, patch2) => setQuery((q) => ({
|
|
517
1058
|
...q,
|
|
518
1059
|
aggregations: (q.aggregations ?? []).map((a) => a.id === id ? mergeAggregation(a, patch2) : a)
|
|
519
1060
|
})),
|
|
520
1061
|
[setQuery]
|
|
521
1062
|
);
|
|
522
|
-
const moveAggregation =
|
|
1063
|
+
const moveAggregation = useCallback6(
|
|
523
1064
|
(id, toIndex) => setQuery((q) => {
|
|
524
1065
|
const current = q.aggregations ?? [];
|
|
525
1066
|
const from = current.findIndex((a) => a.id === id);
|
|
@@ -531,24 +1072,24 @@ function useQueryTable(opts) {
|
|
|
531
1072
|
}),
|
|
532
1073
|
[setQuery]
|
|
533
1074
|
);
|
|
534
|
-
const removeAggregation =
|
|
1075
|
+
const removeAggregation = useCallback6(
|
|
535
1076
|
(id) => setQuery((q) => ({ ...q, aggregations: (q.aggregations ?? []).filter((a) => a.id !== id) })),
|
|
536
1077
|
[setQuery]
|
|
537
1078
|
);
|
|
538
|
-
const clearAggregations =
|
|
1079
|
+
const clearAggregations = useCallback6(
|
|
539
1080
|
() => setQuery((q) => ({ ...q, aggregations: [] })),
|
|
540
1081
|
[setQuery]
|
|
541
1082
|
);
|
|
542
|
-
const setSort =
|
|
543
|
-
const toggleSort =
|
|
1083
|
+
const setSort = useCallback6((orderBy) => setQuery((q) => ({ ...q, offset: 0, orderBy })), [setQuery]);
|
|
1084
|
+
const toggleSort = useCallback6(
|
|
544
1085
|
(field, additive = false) => setQuery((q) => ({ ...q, offset: 0, orderBy: nextOrderBy(q.orderBy, field, additive) })),
|
|
545
1086
|
[setQuery]
|
|
546
1087
|
);
|
|
547
|
-
const setLimit =
|
|
548
|
-
const setOffset =
|
|
549
|
-
const nextPage =
|
|
550
|
-
const prevPage =
|
|
551
|
-
const filterValues =
|
|
1088
|
+
const setLimit = useCallback6((limit) => patch({ limit, offset: 0 }), [patch]);
|
|
1089
|
+
const setOffset = useCallback6((offset) => patch({ offset }), [patch]);
|
|
1090
|
+
const nextPage = useCallback6(() => setQuery((q) => ({ ...q, offset: q.offset + q.limit })), [setQuery]);
|
|
1091
|
+
const prevPage = useCallback6(() => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })), [setQuery]);
|
|
1092
|
+
const filterValues = useCallback6(
|
|
552
1093
|
async (field, search) => {
|
|
553
1094
|
if (!transport?.fetchDistinctValues) {
|
|
554
1095
|
const f = byName.get(field);
|
|
@@ -565,12 +1106,38 @@ function useQueryTable(opts) {
|
|
|
565
1106
|
},
|
|
566
1107
|
[transport, byName, clientRows]
|
|
567
1108
|
);
|
|
568
|
-
const
|
|
1109
|
+
const fieldStats = useCallback6(
|
|
1110
|
+
async (fields, signal) => {
|
|
1111
|
+
const requested = [...new Set(fields)];
|
|
1112
|
+
if (transport?.fetchFieldStats) {
|
|
1113
|
+
const backendFields = requested.filter(
|
|
1114
|
+
(name) => byName.get(name)?.source.kind === "backend"
|
|
1115
|
+
);
|
|
1116
|
+
return backendFields.length ? transport.fetchFieldStats(backendFields, signal) : {};
|
|
1117
|
+
}
|
|
1118
|
+
if (!clientRows) return {};
|
|
1119
|
+
const result = {};
|
|
1120
|
+
for (const name of requested) {
|
|
1121
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
1122
|
+
const field = byName.get(name);
|
|
1123
|
+
if (!field) continue;
|
|
1124
|
+
const distinct = /* @__PURE__ */ new Set();
|
|
1125
|
+
for (const row of clientRows) {
|
|
1126
|
+
const value = readFieldValue2(field, row);
|
|
1127
|
+
if (value !== null) distinct.add(JSON.stringify(value));
|
|
1128
|
+
}
|
|
1129
|
+
result[name] = { distinct: distinct.size };
|
|
1130
|
+
}
|
|
1131
|
+
return result;
|
|
1132
|
+
},
|
|
1133
|
+
[transport, byName, clientRows]
|
|
1134
|
+
);
|
|
1135
|
+
const refresh = useCallback6(() => {
|
|
569
1136
|
setNonce((n) => n + 1);
|
|
570
1137
|
onRefreshRef.current?.();
|
|
571
1138
|
}, []);
|
|
572
|
-
const stopAutoRefresh =
|
|
573
|
-
const startAutoRefresh =
|
|
1139
|
+
const stopAutoRefresh = useCallback6(() => setAutoRefreshStatus(null), []);
|
|
1140
|
+
const startAutoRefresh = useCallback6(
|
|
574
1141
|
({ frequencyMs, turnOffAfterMs }) => {
|
|
575
1142
|
const pollCount = frequencyMs > 0 && turnOffAfterMs > 0 ? Math.floor(turnOffAfterMs / frequencyMs) : 0;
|
|
576
1143
|
if (pollCount < 1 || pollCount > MAX_AUTO_REFRESH_POLLS) {
|
|
@@ -590,7 +1157,7 @@ function useQueryTable(opts) {
|
|
|
590
1157
|
);
|
|
591
1158
|
const autoRefreshFrequencyMs = autoRefreshStatus?.frequencyMs ?? null;
|
|
592
1159
|
const autoRefreshStopsAt = autoRefreshStatus?.stopsAt ?? null;
|
|
593
|
-
|
|
1160
|
+
useEffect4(() => {
|
|
594
1161
|
if (autoRefreshFrequencyMs == null || autoRefreshStopsAt == null) return;
|
|
595
1162
|
const tick = () => {
|
|
596
1163
|
if (nowRef.current() >= autoRefreshStopsAt) {
|
|
@@ -614,7 +1181,7 @@ function useQueryTable(opts) {
|
|
|
614
1181
|
clearTimeout(timeout);
|
|
615
1182
|
};
|
|
616
1183
|
}, [autoRefreshFrequencyMs, autoRefreshStopsAt, refresh]);
|
|
617
|
-
const autoRefresh =
|
|
1184
|
+
const autoRefresh = useMemo6(
|
|
618
1185
|
() => ({
|
|
619
1186
|
status: autoRefreshStatus,
|
|
620
1187
|
start: startAutoRefresh,
|
|
@@ -622,7 +1189,7 @@ function useQueryTable(opts) {
|
|
|
622
1189
|
}),
|
|
623
1190
|
[autoRefreshStatus, startAutoRefresh, stopAutoRefresh]
|
|
624
1191
|
);
|
|
625
|
-
const refreshRow =
|
|
1192
|
+
const refreshRow = useCallback6(
|
|
626
1193
|
async (id) => {
|
|
627
1194
|
if (!transport?.fetchRow) return;
|
|
628
1195
|
const updated = await transport.fetchRow(id);
|
|
@@ -630,8 +1197,8 @@ function useQueryTable(opts) {
|
|
|
630
1197
|
},
|
|
631
1198
|
[transport, rowId]
|
|
632
1199
|
);
|
|
633
|
-
const visibleFields =
|
|
634
|
-
const aggregations =
|
|
1200
|
+
const visibleFields = useMemo6(() => selectedFields2(displaySchema, query), [displaySchema, query]);
|
|
1201
|
+
const aggregations = useMemo6(
|
|
635
1202
|
() => ({
|
|
636
1203
|
clauses: query.aggregations ?? [],
|
|
637
1204
|
results: aggState.results,
|
|
@@ -675,11 +1242,18 @@ function useQueryTable(opts) {
|
|
|
675
1242
|
updateFilter,
|
|
676
1243
|
removeFilter,
|
|
677
1244
|
clearFilters,
|
|
1245
|
+
updatePredicate,
|
|
1246
|
+
removePredicate,
|
|
1247
|
+
negatePredicate,
|
|
1248
|
+
reorderFilters,
|
|
1249
|
+
mergeFilters,
|
|
678
1250
|
resetAll,
|
|
679
1251
|
filterValues,
|
|
1252
|
+
fieldStats,
|
|
680
1253
|
toggleSort,
|
|
681
1254
|
setSort,
|
|
682
1255
|
select,
|
|
1256
|
+
computed,
|
|
683
1257
|
setLimit,
|
|
684
1258
|
setOffset,
|
|
685
1259
|
nextPage,
|
|
@@ -696,7 +1270,7 @@ function nextOrderBy(existing, field, additive) {
|
|
|
696
1270
|
if (!additive) {
|
|
697
1271
|
if (i === 0 && existing.length === 1) {
|
|
698
1272
|
const flipped = existing[0].dir === "desc" ? "asc" : "desc";
|
|
699
|
-
return [{
|
|
1273
|
+
return [{ ...existing[0], dir: flipped }];
|
|
700
1274
|
}
|
|
701
1275
|
return [{ field, dir: "desc" }];
|
|
702
1276
|
}
|
|
@@ -704,12 +1278,14 @@ function nextOrderBy(existing, field, additive) {
|
|
|
704
1278
|
const cur = existing[i];
|
|
705
1279
|
if (cur.dir === "desc") {
|
|
706
1280
|
const next = [...existing];
|
|
707
|
-
next[i] = {
|
|
1281
|
+
next[i] = { ...cur, dir: "asc" };
|
|
708
1282
|
return next;
|
|
709
1283
|
}
|
|
710
1284
|
return existing.filter((_, k) => k !== i);
|
|
711
1285
|
}
|
|
712
1286
|
export {
|
|
1287
|
+
createFormulaWorker,
|
|
1288
|
+
evaluateFormulaRows,
|
|
713
1289
|
useAggregations,
|
|
714
1290
|
useColumnDrag,
|
|
715
1291
|
useQueryTable,
|