@pythia-software/query-table-react 0.1.0 → 0.3.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 +9 -0
- package/dist/index.d.ts +44 -2
- package/dist/index.js +659 -115
- 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
447
|
function useAggregations(query, schema, transport, clientRows, debounceMs, nonce) {
|
|
21
|
-
const [results, setResults] =
|
|
22
|
-
const [loading, setLoading] =
|
|
23
|
-
const [error, setError] =
|
|
24
|
-
const key =
|
|
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);
|
|
@@ -65,11 +492,11 @@ function useAggregations(query, schema, transport, clientRows, debounceMs, nonce
|
|
|
65
492
|
}
|
|
66
493
|
|
|
67
494
|
// src/useSelection.ts
|
|
68
|
-
import { useCallback, useMemo as
|
|
495
|
+
import { useCallback as useCallback2, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react";
|
|
69
496
|
function useSelection(displayedIds) {
|
|
70
|
-
const [selected, setSelected] =
|
|
71
|
-
const anchor =
|
|
72
|
-
const toggle =
|
|
497
|
+
const [selected, setSelected] = useState3(() => /* @__PURE__ */ new Set());
|
|
498
|
+
const anchor = useRef2(null);
|
|
499
|
+
const toggle = useCallback2(
|
|
73
500
|
(id, shiftKey = false) => {
|
|
74
501
|
setSelected((prev) => {
|
|
75
502
|
const next = new Set(prev);
|
|
@@ -97,7 +524,7 @@ function useSelection(displayedIds) {
|
|
|
97
524
|
},
|
|
98
525
|
[displayedIds]
|
|
99
526
|
);
|
|
100
|
-
const setPage =
|
|
527
|
+
const setPage = useCallback2((ids, select) => {
|
|
101
528
|
setSelected((prev) => {
|
|
102
529
|
const next = new Set(prev);
|
|
103
530
|
for (const id of ids) {
|
|
@@ -107,7 +534,21 @@ function useSelection(displayedIds) {
|
|
|
107
534
|
return next;
|
|
108
535
|
});
|
|
109
536
|
}, []);
|
|
110
|
-
const
|
|
537
|
+
const replace = useCallback2((ids) => {
|
|
538
|
+
const next = new Set(ids);
|
|
539
|
+
anchor.current = null;
|
|
540
|
+
setSelected(next);
|
|
541
|
+
}, []);
|
|
542
|
+
const retain = useCallback2((ids) => {
|
|
543
|
+
const allowed = new Set(ids);
|
|
544
|
+
anchor.current = null;
|
|
545
|
+
setSelected((prev) => {
|
|
546
|
+
const next = /* @__PURE__ */ new Set();
|
|
547
|
+
for (const id of prev) if (allowed.has(id)) next.add(id);
|
|
548
|
+
return next;
|
|
549
|
+
});
|
|
550
|
+
}, []);
|
|
551
|
+
const pageState = useCallback2(
|
|
111
552
|
(pageIds) => {
|
|
112
553
|
if (pageIds.length === 0) return "none";
|
|
113
554
|
let n = 0;
|
|
@@ -116,35 +557,37 @@ function useSelection(displayedIds) {
|
|
|
116
557
|
},
|
|
117
558
|
[selected]
|
|
118
559
|
);
|
|
119
|
-
const clear =
|
|
560
|
+
const clear = useCallback2(() => {
|
|
120
561
|
anchor.current = null;
|
|
121
562
|
setSelected(/* @__PURE__ */ new Set());
|
|
122
563
|
}, []);
|
|
123
|
-
return
|
|
564
|
+
return useMemo3(
|
|
124
565
|
() => ({
|
|
125
566
|
selected,
|
|
126
567
|
isSelected: (id) => selected.has(id),
|
|
127
568
|
toggle,
|
|
128
569
|
setPage,
|
|
570
|
+
replace,
|
|
571
|
+
retain,
|
|
129
572
|
pageState,
|
|
130
573
|
clear,
|
|
131
574
|
count: selected.size
|
|
132
575
|
}),
|
|
133
|
-
[selected, toggle, setPage, pageState, clear]
|
|
576
|
+
[selected, toggle, setPage, replace, retain, pageState, clear]
|
|
134
577
|
);
|
|
135
578
|
}
|
|
136
579
|
|
|
137
580
|
// src/useSelect.ts
|
|
138
|
-
import { useCallback as
|
|
581
|
+
import { useCallback as useCallback3, useMemo as useMemo4 } from "react";
|
|
139
582
|
import { selectedFields } from "@pythia-software/query-table-core";
|
|
140
583
|
function useSelect(query, setQuery, schema) {
|
|
141
|
-
const fields =
|
|
142
|
-
const visible =
|
|
143
|
-
const hidden =
|
|
584
|
+
const fields = useMemo4(() => selectedFields(schema, query), [schema, query]);
|
|
585
|
+
const visible = useMemo4(() => fields.map((f) => columnFor(query.select, f.name)), [fields, query.select]);
|
|
586
|
+
const hidden = useMemo4(() => {
|
|
144
587
|
const shown = new Set(fields.map((f) => f.name));
|
|
145
588
|
return schema.fields.filter((f) => !shown.has(f.name) && (f.select?.enabled ?? true));
|
|
146
589
|
}, [schema, fields]);
|
|
147
|
-
const writeSelect =
|
|
590
|
+
const writeSelect = useCallback3(
|
|
148
591
|
(mutate) => {
|
|
149
592
|
setQuery((prev) => {
|
|
150
593
|
const current = prev.select.length ? prev.select : visible;
|
|
@@ -153,9 +596,9 @@ function useSelect(query, setQuery, schema) {
|
|
|
153
596
|
},
|
|
154
597
|
[setQuery, visible]
|
|
155
598
|
);
|
|
156
|
-
const show =
|
|
157
|
-
const hide =
|
|
158
|
-
const move =
|
|
599
|
+
const show = useCallback3((field) => writeSelect((c) => c.some((x) => x.field === field) ? c : [...c, { field }]), [writeSelect]);
|
|
600
|
+
const hide = useCallback3((field) => writeSelect((c) => c.filter((x) => x.field !== field)), [writeSelect]);
|
|
601
|
+
const move = useCallback3(
|
|
159
602
|
(field, toIndex) => writeSelect((c) => {
|
|
160
603
|
const from = c.findIndex((x) => x.field === field);
|
|
161
604
|
if (from === -1) return c;
|
|
@@ -165,11 +608,11 @@ function useSelect(query, setQuery, schema) {
|
|
|
165
608
|
}),
|
|
166
609
|
[writeSelect]
|
|
167
610
|
);
|
|
168
|
-
const setWidth =
|
|
611
|
+
const setWidth = useCallback3(
|
|
169
612
|
(field, width) => writeSelect((c) => c.map((x) => x.field === field ? { ...x, width: Math.round(width) } : x)),
|
|
170
613
|
[writeSelect]
|
|
171
614
|
);
|
|
172
|
-
const reset =
|
|
615
|
+
const reset = useCallback3(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);
|
|
173
616
|
return { visible, fields, hidden, show, hide, move, setWidth, reset };
|
|
174
617
|
}
|
|
175
618
|
function columnFor(select, name) {
|
|
@@ -178,11 +621,11 @@ function columnFor(select, name) {
|
|
|
178
621
|
}
|
|
179
622
|
|
|
180
623
|
// src/useColumnDrag.ts
|
|
181
|
-
import { useCallback as
|
|
624
|
+
import { useCallback as useCallback4, useMemo as useMemo5, useState as useState4 } from "react";
|
|
182
625
|
function useColumnDrag() {
|
|
183
|
-
const [state, setState] =
|
|
184
|
-
const start =
|
|
185
|
-
const over =
|
|
626
|
+
const [state, setState] = useState4(null);
|
|
627
|
+
const start = useCallback4((field, index) => setState({ source: field, overIndex: index }), []);
|
|
628
|
+
const over = useCallback4(
|
|
186
629
|
(index) => (
|
|
187
630
|
// Keep the SAME object reference when unchanged so React can bail out of the
|
|
188
631
|
// re-render — onDragOver fires continuously while the cursor moves.
|
|
@@ -190,8 +633,8 @@ function useColumnDrag() {
|
|
|
190
633
|
),
|
|
191
634
|
[]
|
|
192
635
|
);
|
|
193
|
-
const end =
|
|
194
|
-
const preview =
|
|
636
|
+
const end = useCallback4(() => setState(null), []);
|
|
637
|
+
const preview = useCallback4(
|
|
195
638
|
(order) => {
|
|
196
639
|
if (!state) return order;
|
|
197
640
|
const without = order.filter((n) => n !== state.source);
|
|
@@ -201,7 +644,7 @@ function useColumnDrag() {
|
|
|
201
644
|
},
|
|
202
645
|
[state]
|
|
203
646
|
);
|
|
204
|
-
return
|
|
647
|
+
return useMemo5(
|
|
205
648
|
() => ({
|
|
206
649
|
source: state?.source ?? null,
|
|
207
650
|
overIndex: state?.overIndex ?? null,
|
|
@@ -216,12 +659,12 @@ function useColumnDrag() {
|
|
|
216
659
|
}
|
|
217
660
|
|
|
218
661
|
// src/useSavedQueries.ts
|
|
219
|
-
import { useCallback as
|
|
662
|
+
import { useCallback as useCallback5, useEffect as useEffect3, useState as useState5 } from "react";
|
|
220
663
|
function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
|
|
221
|
-
const [items, setItems] =
|
|
222
|
-
const [defaultId, setDefaultId] =
|
|
223
|
-
const [loading, setLoading] =
|
|
224
|
-
const refresh =
|
|
664
|
+
const [items, setItems] = useState5([]);
|
|
665
|
+
const [defaultId, setDefaultId] = useState5(null);
|
|
666
|
+
const [loading, setLoading] = useState5(true);
|
|
667
|
+
const refresh = useCallback5(async () => {
|
|
225
668
|
setLoading(true);
|
|
226
669
|
try {
|
|
227
670
|
const [savedItems, defaultSaved] = await Promise.all([
|
|
@@ -234,10 +677,10 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
|
|
|
234
677
|
setLoading(false);
|
|
235
678
|
}
|
|
236
679
|
}, [storage, key]);
|
|
237
|
-
|
|
680
|
+
useEffect3(() => {
|
|
238
681
|
void refresh();
|
|
239
682
|
}, [refresh]);
|
|
240
|
-
const save =
|
|
683
|
+
const save = useCallback5(
|
|
241
684
|
async (name) => {
|
|
242
685
|
const saved = await storage.saveNamed(key, name, currentQuery, now());
|
|
243
686
|
await refresh();
|
|
@@ -245,25 +688,25 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
|
|
|
245
688
|
},
|
|
246
689
|
[storage, key, currentQuery, now, refresh]
|
|
247
690
|
);
|
|
248
|
-
const load =
|
|
691
|
+
const load = useCallback5(
|
|
249
692
|
(id) => {
|
|
250
693
|
const found = items.find((q) => q.id === id);
|
|
251
694
|
if (found) applyQueryState(found.query);
|
|
252
695
|
},
|
|
253
696
|
[items, applyQueryState]
|
|
254
697
|
);
|
|
255
|
-
const setDefault =
|
|
698
|
+
const setDefault = useCallback5(
|
|
256
699
|
async (id) => {
|
|
257
700
|
await storage.setDefaultSaved?.(key, id);
|
|
258
701
|
await refresh();
|
|
259
702
|
},
|
|
260
703
|
[storage, key, refresh]
|
|
261
704
|
);
|
|
262
|
-
const clearDefault =
|
|
705
|
+
const clearDefault = useCallback5(async () => {
|
|
263
706
|
await storage.setDefaultSaved?.(key, null);
|
|
264
707
|
await refresh();
|
|
265
708
|
}, [storage, key, refresh]);
|
|
266
|
-
const remove =
|
|
709
|
+
const remove = useCallback5(
|
|
267
710
|
async (id) => {
|
|
268
711
|
if (id === defaultId) await storage.setDefaultSaved?.(key, null);
|
|
269
712
|
await storage.deleteSaved(key, id);
|
|
@@ -286,11 +729,14 @@ function defaultsFor(schema) {
|
|
|
286
729
|
offset: 0
|
|
287
730
|
});
|
|
288
731
|
}
|
|
732
|
+
function cloneWhereTerm(term) {
|
|
733
|
+
return isOrGroup(term) ? { any: term.any.map((c) => ({ ...c })) } : { ...term };
|
|
734
|
+
}
|
|
289
735
|
function cloneQueryState(q) {
|
|
290
736
|
const clone = {
|
|
291
737
|
select: q.select.map((column) => ({ ...column })),
|
|
292
|
-
where: q.where.map(
|
|
293
|
-
orderBy: q.orderBy.map((term) =>
|
|
738
|
+
where: q.where.map(cloneWhereTerm),
|
|
739
|
+
orderBy: q.orderBy.map((term) => term.extract ? { ...term, extract: { ...term.extract } } : { ...term }),
|
|
294
740
|
limit: q.limit,
|
|
295
741
|
offset: q.offset
|
|
296
742
|
};
|
|
@@ -321,7 +767,7 @@ function asDistinctValue(value) {
|
|
|
321
767
|
}
|
|
322
768
|
function fieldHasNullInRows(field, rows) {
|
|
323
769
|
for (const row of rows) {
|
|
324
|
-
if (isNullLike(
|
|
770
|
+
if (isNullLike(readFieldValue2(field, row))) return true;
|
|
325
771
|
}
|
|
326
772
|
return false;
|
|
327
773
|
}
|
|
@@ -331,7 +777,7 @@ function distinctValuesFromRows(field, rows, search) {
|
|
|
331
777
|
const seen = /* @__PURE__ */ new Set();
|
|
332
778
|
let hasMore = false;
|
|
333
779
|
for (const row of rows) {
|
|
334
|
-
const raw = asDistinctValue(
|
|
780
|
+
const raw = asDistinctValue(readFieldValue2(field, row));
|
|
335
781
|
if (raw == null || seen.has(raw)) continue;
|
|
336
782
|
if (target && !raw.toLowerCase().includes(target)) continue;
|
|
337
783
|
if (values.length < AUTOCOMPLETE_LIMIT) {
|
|
@@ -355,30 +801,30 @@ function resolveInitial(opts) {
|
|
|
355
801
|
}
|
|
356
802
|
function useQueryTable(opts) {
|
|
357
803
|
const { schema, transport, clientRows, initialQuery, syncUrl = false, debounceMs = 200 } = opts;
|
|
358
|
-
const storage =
|
|
804
|
+
const storage = useMemo6(() => opts.storage ?? memoryStorageAdapter(), [opts.storage]);
|
|
359
805
|
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 [query, setQueryState] =
|
|
367
|
-
const undoStack =
|
|
368
|
-
const redoStack =
|
|
369
|
-
const [rows, setRows] =
|
|
370
|
-
const [total, setTotal] =
|
|
371
|
-
const [loading, setLoading] =
|
|
372
|
-
const [error, setError] =
|
|
373
|
-
const [nonce, setNonce] =
|
|
374
|
-
const [autoRefreshStatus, setAutoRefreshStatus] =
|
|
375
|
-
|
|
806
|
+
const nowRef = useRef3(now);
|
|
807
|
+
const onRefreshRef = useRef3(opts.onRefresh);
|
|
808
|
+
const defaults = useMemo6(() => defaultsFor(schema), [schema]);
|
|
809
|
+
const byName = useMemo6(() => new Map(schema.fields.map((f) => [f.name, f])), [schema]);
|
|
810
|
+
const initRef = useRef3(resolveInitial(opts));
|
|
811
|
+
const aggIdRef = useRef3(0);
|
|
812
|
+
const [query, setQueryState] = useState6(initRef.current.q);
|
|
813
|
+
const undoStack = useRef3([cloneQueryState(initRef.current.q)]);
|
|
814
|
+
const redoStack = useRef3([]);
|
|
815
|
+
const [rows, setRows] = useState6([]);
|
|
816
|
+
const [total, setTotal] = useState6(null);
|
|
817
|
+
const [loading, setLoading] = useState6(false);
|
|
818
|
+
const [error, setError] = useState6(null);
|
|
819
|
+
const [nonce, setNonce] = useState6(0);
|
|
820
|
+
const [autoRefreshStatus, setAutoRefreshStatus] = useState6(null);
|
|
821
|
+
useEffect4(() => {
|
|
376
822
|
nowRef.current = now;
|
|
377
823
|
}, [now]);
|
|
378
|
-
|
|
824
|
+
useEffect4(() => {
|
|
379
825
|
onRefreshRef.current = opts.onRefresh;
|
|
380
826
|
}, [opts.onRefresh]);
|
|
381
|
-
const setQuery =
|
|
827
|
+
const setQuery = useCallback6((next) => {
|
|
382
828
|
setQueryState((prev) => {
|
|
383
829
|
const candidate = typeof next === "function" ? next(prev) : next;
|
|
384
830
|
const nextQuery = normalizeQueryState(candidate, defaults);
|
|
@@ -388,7 +834,7 @@ function useQueryTable(opts) {
|
|
|
388
834
|
return cloneQueryState(nextQuery);
|
|
389
835
|
});
|
|
390
836
|
}, [defaults]);
|
|
391
|
-
|
|
837
|
+
useEffect4(() => {
|
|
392
838
|
if (initRef.current.fromUrl || initialQuery) return;
|
|
393
839
|
let cancelled = false;
|
|
394
840
|
void (async () => {
|
|
@@ -405,8 +851,22 @@ function useQueryTable(opts) {
|
|
|
405
851
|
cancelled = true;
|
|
406
852
|
};
|
|
407
853
|
}, []);
|
|
408
|
-
const
|
|
409
|
-
|
|
854
|
+
const { api: computed, displaySchema } = useComputedColumns({
|
|
855
|
+
schema,
|
|
856
|
+
query,
|
|
857
|
+
rows,
|
|
858
|
+
...opts.computedColumnStore ? { store: opts.computedColumnStore } : {},
|
|
859
|
+
...opts.formulaWorkerFactory ? { workerFactory: opts.formulaWorkerFactory } : {},
|
|
860
|
+
...transport ? { transport } : {},
|
|
861
|
+
...clientRows ? { clientRows } : {}
|
|
862
|
+
});
|
|
863
|
+
const queryKey = useMemo6(() => encodeQuery(query), [query]);
|
|
864
|
+
const serverRowQuery = useMemo6(() => toServerQuery2(query, displaySchema), [queryKey, displaySchema]);
|
|
865
|
+
const rowQueryKey = useMemo6(
|
|
866
|
+
() => transport ? `server:${JSON.stringify(serverRowQuery)}` : `client:${JSON.stringify([query.where, query.orderBy, query.limit, query.offset])}`,
|
|
867
|
+
[transport, serverRowQuery, query.where, query.orderBy, query.limit, query.offset]
|
|
868
|
+
);
|
|
869
|
+
useEffect4(() => {
|
|
410
870
|
const ac = new AbortController();
|
|
411
871
|
let cancelled = false;
|
|
412
872
|
const run = async () => {
|
|
@@ -414,13 +874,13 @@ function useQueryTable(opts) {
|
|
|
414
874
|
setError(null);
|
|
415
875
|
try {
|
|
416
876
|
if (transport) {
|
|
417
|
-
const res = await transport.fetchRows(
|
|
877
|
+
const res = await transport.fetchRows(serverRowQuery, ac.signal);
|
|
418
878
|
if (!cancelled) {
|
|
419
879
|
setRows(res.rows);
|
|
420
880
|
setTotal(res.total);
|
|
421
881
|
}
|
|
422
882
|
} else {
|
|
423
|
-
const res =
|
|
883
|
+
const res = applyQuery2(clientRows ?? [], query, schema);
|
|
424
884
|
if (!cancelled) {
|
|
425
885
|
setRows(res.rows);
|
|
426
886
|
setTotal(res.total);
|
|
@@ -438,8 +898,8 @@ function useQueryTable(opts) {
|
|
|
438
898
|
clearTimeout(t);
|
|
439
899
|
ac.abort();
|
|
440
900
|
};
|
|
441
|
-
}, [
|
|
442
|
-
|
|
901
|
+
}, [rowQueryKey, transport, clientRows, schema, debounceMs, nonce]);
|
|
902
|
+
useEffect4(() => {
|
|
443
903
|
if (!syncUrl || typeof window === "undefined") return;
|
|
444
904
|
const url = new URL(window.location.href);
|
|
445
905
|
const token = encodeQuery(query);
|
|
@@ -447,18 +907,18 @@ function useQueryTable(opts) {
|
|
|
447
907
|
else url.searchParams.delete("q");
|
|
448
908
|
window.history.replaceState(null, "", url.toString());
|
|
449
909
|
}, [queryKey, syncUrl, query]);
|
|
450
|
-
|
|
910
|
+
useEffect4(() => {
|
|
451
911
|
const t = setTimeout(() => void storage.saveLast(schema.name, query), 400);
|
|
452
912
|
return () => clearTimeout(t);
|
|
453
913
|
}, [queryKey, storage, schema.name, query]);
|
|
454
|
-
const idField =
|
|
455
|
-
const rowId =
|
|
456
|
-
(row) => idField ?
|
|
914
|
+
const idField = useMemo6(() => schema.fields.find((f) => f.name === schema.idField), [schema]);
|
|
915
|
+
const rowId = useCallback6(
|
|
916
|
+
(row) => idField ? readFieldValue2(idField, row) : null,
|
|
457
917
|
[idField]
|
|
458
918
|
);
|
|
459
|
-
const displayedIds =
|
|
919
|
+
const displayedIds = useMemo6(() => rows.map(rowId).filter((id) => id != null), [rows, rowId]);
|
|
460
920
|
const selection = useSelection(displayedIds);
|
|
461
|
-
const select = useSelect(query, setQuery,
|
|
921
|
+
const select = useSelect(query, setQuery, displaySchema);
|
|
462
922
|
const columnDrag = useColumnDrag();
|
|
463
923
|
const saved = useSavedQueries(
|
|
464
924
|
schema.name,
|
|
@@ -470,7 +930,7 @@ function useQueryTable(opts) {
|
|
|
470
930
|
const aggState = useAggregations(query, schema, transport, clientRows, debounceMs, nonce);
|
|
471
931
|
const canUndo = undoStack.current.length > 0;
|
|
472
932
|
const canRedo = redoStack.current.length > 0;
|
|
473
|
-
const undo =
|
|
933
|
+
const undo = useCallback6(() => {
|
|
474
934
|
setQueryState((prev) => {
|
|
475
935
|
const previous = undoStack.current.pop();
|
|
476
936
|
if (!previous) return prev;
|
|
@@ -478,7 +938,7 @@ function useQueryTable(opts) {
|
|
|
478
938
|
return cloneQueryState(previous);
|
|
479
939
|
});
|
|
480
940
|
}, []);
|
|
481
|
-
const redo =
|
|
941
|
+
const redo = useCallback6(() => {
|
|
482
942
|
setQueryState((prev) => {
|
|
483
943
|
const next = redoStack.current.pop();
|
|
484
944
|
if (!next) return prev;
|
|
@@ -486,19 +946,95 @@ function useQueryTable(opts) {
|
|
|
486
946
|
return cloneQueryState(next);
|
|
487
947
|
});
|
|
488
948
|
}, []);
|
|
489
|
-
const patch =
|
|
490
|
-
const addFilter =
|
|
491
|
-
const updateFilter =
|
|
492
|
-
(index, clause) => setQuery((q) => ({ ...q, offset: 0, where: q.where.map((
|
|
949
|
+
const patch = useCallback6((p) => setQuery((prev) => ({ ...prev, ...p })), [setQuery]);
|
|
950
|
+
const addFilter = useCallback6((clause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })), [setQuery]);
|
|
951
|
+
const updateFilter = useCallback6(
|
|
952
|
+
(index, clause) => setQuery((q) => ({ ...q, offset: 0, where: q.where.map((t, i) => i === index ? clause : t) })),
|
|
493
953
|
[setQuery]
|
|
494
954
|
);
|
|
495
|
-
const removeFilter =
|
|
955
|
+
const removeFilter = useCallback6(
|
|
496
956
|
(index) => setQuery((q) => ({ ...q, offset: 0, where: q.where.filter((_, i) => i !== index) })),
|
|
497
957
|
[setQuery]
|
|
498
958
|
);
|
|
499
|
-
const clearFilters =
|
|
500
|
-
const
|
|
501
|
-
|
|
959
|
+
const clearFilters = useCallback6(() => setQuery((q) => ({ ...q, offset: 0, where: [] })), [setQuery]);
|
|
960
|
+
const updatePredicate = useCallback6(
|
|
961
|
+
(termIndex, predIndex, clause) => setQuery((q) => ({
|
|
962
|
+
...q,
|
|
963
|
+
offset: 0,
|
|
964
|
+
where: q.where.map((term, i) => {
|
|
965
|
+
if (i !== termIndex) return term;
|
|
966
|
+
if (isOrGroup(term)) return { any: term.any.map((c, j) => j === predIndex ? clause : c) };
|
|
967
|
+
return clause;
|
|
968
|
+
})
|
|
969
|
+
})),
|
|
970
|
+
[setQuery]
|
|
971
|
+
);
|
|
972
|
+
const removePredicate = useCallback6(
|
|
973
|
+
(termIndex, predIndex) => setQuery((q) => {
|
|
974
|
+
const where = [];
|
|
975
|
+
q.where.forEach((term, i) => {
|
|
976
|
+
if (i !== termIndex) {
|
|
977
|
+
where.push(term);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (isOrGroup(term)) {
|
|
981
|
+
const members = term.any.filter((_, j) => j !== predIndex);
|
|
982
|
+
if (members.length === 1) where.push(members[0]);
|
|
983
|
+
else if (members.length > 1) where.push({ any: members });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
return { ...q, offset: 0, where };
|
|
988
|
+
}),
|
|
989
|
+
[setQuery]
|
|
990
|
+
);
|
|
991
|
+
const negateOne = useCallback6(
|
|
992
|
+
(clause) => {
|
|
993
|
+
const field = byName.get(clause.field);
|
|
994
|
+
return negateClause(clause, field ? opsForField(field) : void 0);
|
|
995
|
+
},
|
|
996
|
+
[byName]
|
|
997
|
+
);
|
|
998
|
+
const negatePredicate = useCallback6(
|
|
999
|
+
(termIndex, predIndex) => setQuery((q) => ({
|
|
1000
|
+
...q,
|
|
1001
|
+
offset: 0,
|
|
1002
|
+
where: q.where.map((term, i) => {
|
|
1003
|
+
if (i !== termIndex) return term;
|
|
1004
|
+
if (isOrGroup(term)) return { any: term.any.map((c, j) => j === predIndex ? negateOne(c) : c) };
|
|
1005
|
+
return negateOne(term);
|
|
1006
|
+
})
|
|
1007
|
+
})),
|
|
1008
|
+
[setQuery, negateOne]
|
|
1009
|
+
);
|
|
1010
|
+
const reorderFilters = useCallback6(
|
|
1011
|
+
(fromIndex, toIndex) => setQuery((q) => {
|
|
1012
|
+
if (fromIndex < 0 || fromIndex >= q.where.length) return q;
|
|
1013
|
+
const where = [...q.where];
|
|
1014
|
+
const [moved] = where.splice(fromIndex, 1);
|
|
1015
|
+
where.splice(Math.max(0, Math.min(toIndex, where.length)), 0, moved);
|
|
1016
|
+
return { ...q, offset: 0, where };
|
|
1017
|
+
}),
|
|
1018
|
+
[setQuery]
|
|
1019
|
+
);
|
|
1020
|
+
const mergeFilters = useCallback6(
|
|
1021
|
+
(sourceIndex, targetIndex) => setQuery((q) => {
|
|
1022
|
+
if (sourceIndex === targetIndex) return q;
|
|
1023
|
+
const source = q.where[sourceIndex];
|
|
1024
|
+
const target = q.where[targetIndex];
|
|
1025
|
+
if (!source || !target) return q;
|
|
1026
|
+
const merged = { any: [...predicatesOf(target), ...predicatesOf(source)] };
|
|
1027
|
+
const where = [];
|
|
1028
|
+
q.where.forEach((term, i) => {
|
|
1029
|
+
if (i === sourceIndex) return;
|
|
1030
|
+
where.push(i === targetIndex ? merged : term);
|
|
1031
|
+
});
|
|
1032
|
+
return { ...q, offset: 0, where };
|
|
1033
|
+
}),
|
|
1034
|
+
[setQuery]
|
|
1035
|
+
);
|
|
1036
|
+
const resetAll = useCallback6(() => setQuery(() => cloneQueryState(defaults)), [setQuery, defaults]);
|
|
1037
|
+
const addAggregation = useCallback6(
|
|
502
1038
|
(partial) => {
|
|
503
1039
|
const id = `a${nowRef.current()}-${aggIdRef.current++}`;
|
|
504
1040
|
const clause = { id, op: "count", groupBy: [], ...partial };
|
|
@@ -507,14 +1043,14 @@ function useQueryTable(opts) {
|
|
|
507
1043
|
},
|
|
508
1044
|
[setQuery]
|
|
509
1045
|
);
|
|
510
|
-
const updateAggregation =
|
|
1046
|
+
const updateAggregation = useCallback6(
|
|
511
1047
|
(id, patch2) => setQuery((q) => ({
|
|
512
1048
|
...q,
|
|
513
1049
|
aggregations: (q.aggregations ?? []).map((a) => a.id === id ? mergeAggregation(a, patch2) : a)
|
|
514
1050
|
})),
|
|
515
1051
|
[setQuery]
|
|
516
1052
|
);
|
|
517
|
-
const moveAggregation =
|
|
1053
|
+
const moveAggregation = useCallback6(
|
|
518
1054
|
(id, toIndex) => setQuery((q) => {
|
|
519
1055
|
const current = q.aggregations ?? [];
|
|
520
1056
|
const from = current.findIndex((a) => a.id === id);
|
|
@@ -526,24 +1062,24 @@ function useQueryTable(opts) {
|
|
|
526
1062
|
}),
|
|
527
1063
|
[setQuery]
|
|
528
1064
|
);
|
|
529
|
-
const removeAggregation =
|
|
1065
|
+
const removeAggregation = useCallback6(
|
|
530
1066
|
(id) => setQuery((q) => ({ ...q, aggregations: (q.aggregations ?? []).filter((a) => a.id !== id) })),
|
|
531
1067
|
[setQuery]
|
|
532
1068
|
);
|
|
533
|
-
const clearAggregations =
|
|
1069
|
+
const clearAggregations = useCallback6(
|
|
534
1070
|
() => setQuery((q) => ({ ...q, aggregations: [] })),
|
|
535
1071
|
[setQuery]
|
|
536
1072
|
);
|
|
537
|
-
const setSort =
|
|
538
|
-
const toggleSort =
|
|
1073
|
+
const setSort = useCallback6((orderBy) => setQuery((q) => ({ ...q, offset: 0, orderBy })), [setQuery]);
|
|
1074
|
+
const toggleSort = useCallback6(
|
|
539
1075
|
(field, additive = false) => setQuery((q) => ({ ...q, offset: 0, orderBy: nextOrderBy(q.orderBy, field, additive) })),
|
|
540
1076
|
[setQuery]
|
|
541
1077
|
);
|
|
542
|
-
const setLimit =
|
|
543
|
-
const setOffset =
|
|
544
|
-
const nextPage =
|
|
545
|
-
const prevPage =
|
|
546
|
-
const filterValues =
|
|
1078
|
+
const setLimit = useCallback6((limit) => patch({ limit, offset: 0 }), [patch]);
|
|
1079
|
+
const setOffset = useCallback6((offset) => patch({ offset }), [patch]);
|
|
1080
|
+
const nextPage = useCallback6(() => setQuery((q) => ({ ...q, offset: q.offset + q.limit })), [setQuery]);
|
|
1081
|
+
const prevPage = useCallback6(() => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })), [setQuery]);
|
|
1082
|
+
const filterValues = useCallback6(
|
|
547
1083
|
async (field, search) => {
|
|
548
1084
|
if (!transport?.fetchDistinctValues) {
|
|
549
1085
|
const f = byName.get(field);
|
|
@@ -560,12 +1096,12 @@ function useQueryTable(opts) {
|
|
|
560
1096
|
},
|
|
561
1097
|
[transport, byName, clientRows]
|
|
562
1098
|
);
|
|
563
|
-
const refresh =
|
|
1099
|
+
const refresh = useCallback6(() => {
|
|
564
1100
|
setNonce((n) => n + 1);
|
|
565
1101
|
onRefreshRef.current?.();
|
|
566
1102
|
}, []);
|
|
567
|
-
const stopAutoRefresh =
|
|
568
|
-
const startAutoRefresh =
|
|
1103
|
+
const stopAutoRefresh = useCallback6(() => setAutoRefreshStatus(null), []);
|
|
1104
|
+
const startAutoRefresh = useCallback6(
|
|
569
1105
|
({ frequencyMs, turnOffAfterMs }) => {
|
|
570
1106
|
const pollCount = frequencyMs > 0 && turnOffAfterMs > 0 ? Math.floor(turnOffAfterMs / frequencyMs) : 0;
|
|
571
1107
|
if (pollCount < 1 || pollCount > MAX_AUTO_REFRESH_POLLS) {
|
|
@@ -585,7 +1121,7 @@ function useQueryTable(opts) {
|
|
|
585
1121
|
);
|
|
586
1122
|
const autoRefreshFrequencyMs = autoRefreshStatus?.frequencyMs ?? null;
|
|
587
1123
|
const autoRefreshStopsAt = autoRefreshStatus?.stopsAt ?? null;
|
|
588
|
-
|
|
1124
|
+
useEffect4(() => {
|
|
589
1125
|
if (autoRefreshFrequencyMs == null || autoRefreshStopsAt == null) return;
|
|
590
1126
|
const tick = () => {
|
|
591
1127
|
if (nowRef.current() >= autoRefreshStopsAt) {
|
|
@@ -609,7 +1145,7 @@ function useQueryTable(opts) {
|
|
|
609
1145
|
clearTimeout(timeout);
|
|
610
1146
|
};
|
|
611
1147
|
}, [autoRefreshFrequencyMs, autoRefreshStopsAt, refresh]);
|
|
612
|
-
const autoRefresh =
|
|
1148
|
+
const autoRefresh = useMemo6(
|
|
613
1149
|
() => ({
|
|
614
1150
|
status: autoRefreshStatus,
|
|
615
1151
|
start: startAutoRefresh,
|
|
@@ -617,7 +1153,7 @@ function useQueryTable(opts) {
|
|
|
617
1153
|
}),
|
|
618
1154
|
[autoRefreshStatus, startAutoRefresh, stopAutoRefresh]
|
|
619
1155
|
);
|
|
620
|
-
const refreshRow =
|
|
1156
|
+
const refreshRow = useCallback6(
|
|
621
1157
|
async (id) => {
|
|
622
1158
|
if (!transport?.fetchRow) return;
|
|
623
1159
|
const updated = await transport.fetchRow(id);
|
|
@@ -625,8 +1161,8 @@ function useQueryTable(opts) {
|
|
|
625
1161
|
},
|
|
626
1162
|
[transport, rowId]
|
|
627
1163
|
);
|
|
628
|
-
const visibleFields =
|
|
629
|
-
const aggregations =
|
|
1164
|
+
const visibleFields = useMemo6(() => selectedFields2(displaySchema, query), [displaySchema, query]);
|
|
1165
|
+
const aggregations = useMemo6(
|
|
630
1166
|
() => ({
|
|
631
1167
|
clauses: query.aggregations ?? [],
|
|
632
1168
|
results: aggState.results,
|
|
@@ -670,11 +1206,17 @@ function useQueryTable(opts) {
|
|
|
670
1206
|
updateFilter,
|
|
671
1207
|
removeFilter,
|
|
672
1208
|
clearFilters,
|
|
1209
|
+
updatePredicate,
|
|
1210
|
+
removePredicate,
|
|
1211
|
+
negatePredicate,
|
|
1212
|
+
reorderFilters,
|
|
1213
|
+
mergeFilters,
|
|
673
1214
|
resetAll,
|
|
674
1215
|
filterValues,
|
|
675
1216
|
toggleSort,
|
|
676
1217
|
setSort,
|
|
677
1218
|
select,
|
|
1219
|
+
computed,
|
|
678
1220
|
setLimit,
|
|
679
1221
|
setOffset,
|
|
680
1222
|
nextPage,
|
|
@@ -691,7 +1233,7 @@ function nextOrderBy(existing, field, additive) {
|
|
|
691
1233
|
if (!additive) {
|
|
692
1234
|
if (i === 0 && existing.length === 1) {
|
|
693
1235
|
const flipped = existing[0].dir === "desc" ? "asc" : "desc";
|
|
694
|
-
return [{
|
|
1236
|
+
return [{ ...existing[0], dir: flipped }];
|
|
695
1237
|
}
|
|
696
1238
|
return [{ field, dir: "desc" }];
|
|
697
1239
|
}
|
|
@@ -699,12 +1241,14 @@ function nextOrderBy(existing, field, additive) {
|
|
|
699
1241
|
const cur = existing[i];
|
|
700
1242
|
if (cur.dir === "desc") {
|
|
701
1243
|
const next = [...existing];
|
|
702
|
-
next[i] = {
|
|
1244
|
+
next[i] = { ...cur, dir: "asc" };
|
|
703
1245
|
return next;
|
|
704
1246
|
}
|
|
705
1247
|
return existing.filter((_, k) => k !== i);
|
|
706
1248
|
}
|
|
707
1249
|
export {
|
|
1250
|
+
createFormulaWorker,
|
|
1251
|
+
evaluateFormulaRows,
|
|
708
1252
|
useAggregations,
|
|
709
1253
|
useColumnDrag,
|
|
710
1254
|
useQueryTable,
|