@pythia-software/query-table-react 0.2.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/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 useCallback5, useEffect as useEffect3, useMemo as useMemo5, useRef as useRef2, useState as useState5 } from "react";
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] = useState(null);
22
- const [loading, setLoading] = useState(false);
23
- const [error, setError] = useState(null);
24
- const key = useMemo(
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
- useEffect(() => {
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 useMemo2, useRef, useState as useState2 } from "react";
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] = useState2(() => /* @__PURE__ */ new Set());
71
- const anchor = useRef(null);
72
- const toggle = useCallback(
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 = useCallback((ids, select) => {
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 pageState = useCallback(
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 = useCallback(() => {
560
+ const clear = useCallback2(() => {
120
561
  anchor.current = null;
121
562
  setSelected(/* @__PURE__ */ new Set());
122
563
  }, []);
123
- return useMemo2(
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 useCallback2, useMemo as useMemo3 } from "react";
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 = useMemo3(() => selectedFields(schema, query), [schema, query]);
142
- const visible = useMemo3(() => fields.map((f) => columnFor(query.select, f.name)), [fields, query.select]);
143
- const hidden = useMemo3(() => {
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 = useCallback2(
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 = useCallback2((field) => writeSelect((c) => c.some((x) => x.field === field) ? c : [...c, { field }]), [writeSelect]);
157
- const hide = useCallback2((field) => writeSelect((c) => c.filter((x) => x.field !== field)), [writeSelect]);
158
- const move = useCallback2(
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 = useCallback2(
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 = useCallback2(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);
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 useCallback3, useMemo as useMemo4, useState as useState3 } from "react";
624
+ import { useCallback as useCallback4, useMemo as useMemo5, useState as useState4 } from "react";
182
625
  function useColumnDrag() {
183
- const [state, setState] = useState3(null);
184
- const start = useCallback3((field, index) => setState({ source: field, overIndex: index }), []);
185
- const over = useCallback3(
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 = useCallback3(() => setState(null), []);
194
- const preview = useCallback3(
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 useMemo4(
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 useCallback4, useEffect as useEffect2, useState as useState4 } from "react";
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] = useState4([]);
222
- const [defaultId, setDefaultId] = useState4(null);
223
- const [loading, setLoading] = useState4(true);
224
- const refresh = useCallback4(async () => {
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
- useEffect2(() => {
680
+ useEffect3(() => {
238
681
  void refresh();
239
682
  }, [refresh]);
240
- const save = useCallback4(
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 = useCallback4(
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 = useCallback4(
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 = useCallback4(async () => {
705
+ const clearDefault = useCallback5(async () => {
263
706
  await storage.setDefaultSaved?.(key, null);
264
707
  await refresh();
265
708
  }, [storage, key, refresh]);
266
- const remove = useCallback4(
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((clause) => ({ ...clause })),
293
- orderBy: q.orderBy.map((term) => ({ ...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(readFieldValue(field, row))) return true;
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(readFieldValue(field, row));
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 = useMemo5(() => opts.storage ?? memoryStorageAdapter(), [opts.storage]);
804
+ const storage = useMemo6(() => opts.storage ?? memoryStorageAdapter(), [opts.storage]);
359
805
  const now = opts.now ?? Date.now;
360
- const nowRef = useRef2(now);
361
- const onRefreshRef = useRef2(opts.onRefresh);
362
- const defaults = useMemo5(() => defaultsFor(schema), [schema]);
363
- const byName = useMemo5(() => new Map(schema.fields.map((f) => [f.name, f])), [schema]);
364
- const initRef = useRef2(resolveInitial(opts));
365
- const aggIdRef = useRef2(0);
366
- const [query, setQueryState] = useState5(initRef.current.q);
367
- const undoStack = useRef2([cloneQueryState(initRef.current.q)]);
368
- const redoStack = useRef2([]);
369
- const [rows, setRows] = useState5([]);
370
- const [total, setTotal] = useState5(null);
371
- const [loading, setLoading] = useState5(false);
372
- const [error, setError] = useState5(null);
373
- const [nonce, setNonce] = useState5(0);
374
- const [autoRefreshStatus, setAutoRefreshStatus] = useState5(null);
375
- useEffect3(() => {
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
- useEffect3(() => {
824
+ useEffect4(() => {
379
825
  onRefreshRef.current = opts.onRefresh;
380
826
  }, [opts.onRefresh]);
381
- const setQuery = useCallback5((next) => {
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
- useEffect3(() => {
837
+ useEffect4(() => {
392
838
  if (initRef.current.fromUrl || initialQuery) return;
393
839
  let cancelled = false;
394
840
  void (async () => {
@@ -405,13 +851,22 @@ function useQueryTable(opts) {
405
851
  cancelled = true;
406
852
  };
407
853
  }, []);
408
- const queryKey = useMemo5(() => encodeQuery(query), [query]);
409
- const serverRowQuery = useMemo5(() => toServerQuery(query, schema), [queryKey, schema]);
410
- const rowQueryKey = useMemo5(
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(
411
866
  () => transport ? `server:${JSON.stringify(serverRowQuery)}` : `client:${JSON.stringify([query.where, query.orderBy, query.limit, query.offset])}`,
412
867
  [transport, serverRowQuery, query.where, query.orderBy, query.limit, query.offset]
413
868
  );
414
- useEffect3(() => {
869
+ useEffect4(() => {
415
870
  const ac = new AbortController();
416
871
  let cancelled = false;
417
872
  const run = async () => {
@@ -425,7 +880,7 @@ function useQueryTable(opts) {
425
880
  setTotal(res.total);
426
881
  }
427
882
  } else {
428
- const res = applyQuery(clientRows ?? [], query, schema);
883
+ const res = applyQuery2(clientRows ?? [], query, schema);
429
884
  if (!cancelled) {
430
885
  setRows(res.rows);
431
886
  setTotal(res.total);
@@ -444,7 +899,7 @@ function useQueryTable(opts) {
444
899
  ac.abort();
445
900
  };
446
901
  }, [rowQueryKey, transport, clientRows, schema, debounceMs, nonce]);
447
- useEffect3(() => {
902
+ useEffect4(() => {
448
903
  if (!syncUrl || typeof window === "undefined") return;
449
904
  const url = new URL(window.location.href);
450
905
  const token = encodeQuery(query);
@@ -452,18 +907,18 @@ function useQueryTable(opts) {
452
907
  else url.searchParams.delete("q");
453
908
  window.history.replaceState(null, "", url.toString());
454
909
  }, [queryKey, syncUrl, query]);
455
- useEffect3(() => {
910
+ useEffect4(() => {
456
911
  const t = setTimeout(() => void storage.saveLast(schema.name, query), 400);
457
912
  return () => clearTimeout(t);
458
913
  }, [queryKey, storage, schema.name, query]);
459
- const idField = useMemo5(() => schema.fields.find((f) => f.name === schema.idField), [schema]);
460
- const rowId = useCallback5(
461
- (row) => idField ? readFieldValue(idField, row) : null,
914
+ const idField = useMemo6(() => schema.fields.find((f) => f.name === schema.idField), [schema]);
915
+ const rowId = useCallback6(
916
+ (row) => idField ? readFieldValue2(idField, row) : null,
462
917
  [idField]
463
918
  );
464
- const displayedIds = useMemo5(() => rows.map(rowId).filter((id) => id != null), [rows, rowId]);
919
+ const displayedIds = useMemo6(() => rows.map(rowId).filter((id) => id != null), [rows, rowId]);
465
920
  const selection = useSelection(displayedIds);
466
- const select = useSelect(query, setQuery, schema);
921
+ const select = useSelect(query, setQuery, displaySchema);
467
922
  const columnDrag = useColumnDrag();
468
923
  const saved = useSavedQueries(
469
924
  schema.name,
@@ -475,7 +930,7 @@ function useQueryTable(opts) {
475
930
  const aggState = useAggregations(query, schema, transport, clientRows, debounceMs, nonce);
476
931
  const canUndo = undoStack.current.length > 0;
477
932
  const canRedo = redoStack.current.length > 0;
478
- const undo = useCallback5(() => {
933
+ const undo = useCallback6(() => {
479
934
  setQueryState((prev) => {
480
935
  const previous = undoStack.current.pop();
481
936
  if (!previous) return prev;
@@ -483,7 +938,7 @@ function useQueryTable(opts) {
483
938
  return cloneQueryState(previous);
484
939
  });
485
940
  }, []);
486
- const redo = useCallback5(() => {
941
+ const redo = useCallback6(() => {
487
942
  setQueryState((prev) => {
488
943
  const next = redoStack.current.pop();
489
944
  if (!next) return prev;
@@ -491,19 +946,95 @@ function useQueryTable(opts) {
491
946
  return cloneQueryState(next);
492
947
  });
493
948
  }, []);
494
- const patch = useCallback5((p) => setQuery((prev) => ({ ...prev, ...p })), [setQuery]);
495
- const addFilter = useCallback5((clause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })), [setQuery]);
496
- const updateFilter = useCallback5(
497
- (index, clause) => setQuery((q) => ({ ...q, offset: 0, where: q.where.map((c, i) => i === index ? clause : c) })),
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) })),
498
953
  [setQuery]
499
954
  );
500
- const removeFilter = useCallback5(
955
+ const removeFilter = useCallback6(
501
956
  (index) => setQuery((q) => ({ ...q, offset: 0, where: q.where.filter((_, i) => i !== index) })),
502
957
  [setQuery]
503
958
  );
504
- const clearFilters = useCallback5(() => setQuery((q) => ({ ...q, offset: 0, where: [] })), [setQuery]);
505
- const resetAll = useCallback5(() => setQuery(() => cloneQueryState(defaults)), [setQuery, defaults]);
506
- const addAggregation = useCallback5(
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(
507
1038
  (partial) => {
508
1039
  const id = `a${nowRef.current()}-${aggIdRef.current++}`;
509
1040
  const clause = { id, op: "count", groupBy: [], ...partial };
@@ -512,14 +1043,14 @@ function useQueryTable(opts) {
512
1043
  },
513
1044
  [setQuery]
514
1045
  );
515
- const updateAggregation = useCallback5(
1046
+ const updateAggregation = useCallback6(
516
1047
  (id, patch2) => setQuery((q) => ({
517
1048
  ...q,
518
1049
  aggregations: (q.aggregations ?? []).map((a) => a.id === id ? mergeAggregation(a, patch2) : a)
519
1050
  })),
520
1051
  [setQuery]
521
1052
  );
522
- const moveAggregation = useCallback5(
1053
+ const moveAggregation = useCallback6(
523
1054
  (id, toIndex) => setQuery((q) => {
524
1055
  const current = q.aggregations ?? [];
525
1056
  const from = current.findIndex((a) => a.id === id);
@@ -531,24 +1062,24 @@ function useQueryTable(opts) {
531
1062
  }),
532
1063
  [setQuery]
533
1064
  );
534
- const removeAggregation = useCallback5(
1065
+ const removeAggregation = useCallback6(
535
1066
  (id) => setQuery((q) => ({ ...q, aggregations: (q.aggregations ?? []).filter((a) => a.id !== id) })),
536
1067
  [setQuery]
537
1068
  );
538
- const clearAggregations = useCallback5(
1069
+ const clearAggregations = useCallback6(
539
1070
  () => setQuery((q) => ({ ...q, aggregations: [] })),
540
1071
  [setQuery]
541
1072
  );
542
- const setSort = useCallback5((orderBy) => setQuery((q) => ({ ...q, offset: 0, orderBy })), [setQuery]);
543
- const toggleSort = useCallback5(
1073
+ const setSort = useCallback6((orderBy) => setQuery((q) => ({ ...q, offset: 0, orderBy })), [setQuery]);
1074
+ const toggleSort = useCallback6(
544
1075
  (field, additive = false) => setQuery((q) => ({ ...q, offset: 0, orderBy: nextOrderBy(q.orderBy, field, additive) })),
545
1076
  [setQuery]
546
1077
  );
547
- const setLimit = useCallback5((limit) => patch({ limit, offset: 0 }), [patch]);
548
- const setOffset = useCallback5((offset) => patch({ offset }), [patch]);
549
- const nextPage = useCallback5(() => setQuery((q) => ({ ...q, offset: q.offset + q.limit })), [setQuery]);
550
- const prevPage = useCallback5(() => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })), [setQuery]);
551
- const filterValues = useCallback5(
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(
552
1083
  async (field, search) => {
553
1084
  if (!transport?.fetchDistinctValues) {
554
1085
  const f = byName.get(field);
@@ -565,12 +1096,12 @@ function useQueryTable(opts) {
565
1096
  },
566
1097
  [transport, byName, clientRows]
567
1098
  );
568
- const refresh = useCallback5(() => {
1099
+ const refresh = useCallback6(() => {
569
1100
  setNonce((n) => n + 1);
570
1101
  onRefreshRef.current?.();
571
1102
  }, []);
572
- const stopAutoRefresh = useCallback5(() => setAutoRefreshStatus(null), []);
573
- const startAutoRefresh = useCallback5(
1103
+ const stopAutoRefresh = useCallback6(() => setAutoRefreshStatus(null), []);
1104
+ const startAutoRefresh = useCallback6(
574
1105
  ({ frequencyMs, turnOffAfterMs }) => {
575
1106
  const pollCount = frequencyMs > 0 && turnOffAfterMs > 0 ? Math.floor(turnOffAfterMs / frequencyMs) : 0;
576
1107
  if (pollCount < 1 || pollCount > MAX_AUTO_REFRESH_POLLS) {
@@ -590,7 +1121,7 @@ function useQueryTable(opts) {
590
1121
  );
591
1122
  const autoRefreshFrequencyMs = autoRefreshStatus?.frequencyMs ?? null;
592
1123
  const autoRefreshStopsAt = autoRefreshStatus?.stopsAt ?? null;
593
- useEffect3(() => {
1124
+ useEffect4(() => {
594
1125
  if (autoRefreshFrequencyMs == null || autoRefreshStopsAt == null) return;
595
1126
  const tick = () => {
596
1127
  if (nowRef.current() >= autoRefreshStopsAt) {
@@ -614,7 +1145,7 @@ function useQueryTable(opts) {
614
1145
  clearTimeout(timeout);
615
1146
  };
616
1147
  }, [autoRefreshFrequencyMs, autoRefreshStopsAt, refresh]);
617
- const autoRefresh = useMemo5(
1148
+ const autoRefresh = useMemo6(
618
1149
  () => ({
619
1150
  status: autoRefreshStatus,
620
1151
  start: startAutoRefresh,
@@ -622,7 +1153,7 @@ function useQueryTable(opts) {
622
1153
  }),
623
1154
  [autoRefreshStatus, startAutoRefresh, stopAutoRefresh]
624
1155
  );
625
- const refreshRow = useCallback5(
1156
+ const refreshRow = useCallback6(
626
1157
  async (id) => {
627
1158
  if (!transport?.fetchRow) return;
628
1159
  const updated = await transport.fetchRow(id);
@@ -630,8 +1161,8 @@ function useQueryTable(opts) {
630
1161
  },
631
1162
  [transport, rowId]
632
1163
  );
633
- const visibleFields = useMemo5(() => selectedFields2(schema, query), [schema, query]);
634
- const aggregations = useMemo5(
1164
+ const visibleFields = useMemo6(() => selectedFields2(displaySchema, query), [displaySchema, query]);
1165
+ const aggregations = useMemo6(
635
1166
  () => ({
636
1167
  clauses: query.aggregations ?? [],
637
1168
  results: aggState.results,
@@ -675,11 +1206,17 @@ function useQueryTable(opts) {
675
1206
  updateFilter,
676
1207
  removeFilter,
677
1208
  clearFilters,
1209
+ updatePredicate,
1210
+ removePredicate,
1211
+ negatePredicate,
1212
+ reorderFilters,
1213
+ mergeFilters,
678
1214
  resetAll,
679
1215
  filterValues,
680
1216
  toggleSort,
681
1217
  setSort,
682
1218
  select,
1219
+ computed,
683
1220
  setLimit,
684
1221
  setOffset,
685
1222
  nextPage,
@@ -696,7 +1233,7 @@ function nextOrderBy(existing, field, additive) {
696
1233
  if (!additive) {
697
1234
  if (i === 0 && existing.length === 1) {
698
1235
  const flipped = existing[0].dir === "desc" ? "asc" : "desc";
699
- return [{ field, dir: flipped }];
1236
+ return [{ ...existing[0], dir: flipped }];
700
1237
  }
701
1238
  return [{ field, dir: "desc" }];
702
1239
  }
@@ -704,12 +1241,14 @@ function nextOrderBy(existing, field, additive) {
704
1241
  const cur = existing[i];
705
1242
  if (cur.dir === "desc") {
706
1243
  const next = [...existing];
707
- next[i] = { field, dir: "asc" };
1244
+ next[i] = { ...cur, dir: "asc" };
708
1245
  return next;
709
1246
  }
710
1247
  return existing.filter((_, k) => k !== i);
711
1248
  }
712
1249
  export {
1250
+ createFormulaWorker,
1251
+ evaluateFormulaRows,
713
1252
  useAggregations,
714
1253
  useColumnDrag,
715
1254
  useQueryTable,