@pythia-software/query-table-ui 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/dist/index.js CHANGED
@@ -137,17 +137,72 @@ function formatRelative(iso) {
137
137
  }
138
138
 
139
139
  // src/DataTable.tsx
140
- import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
140
+ import { isComputedCellError } from "@pythia-software/query-table-core";
141
+ import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState3 } from "react";
141
142
  import { useVirtualizer } from "@tanstack/react-virtual";
142
143
  import { isSortable, readFieldValue } from "@pythia-software/query-table-core";
143
144
  import { useColumnDrag } from "@pythia-software/query-table-react";
144
145
 
146
+ // src/FilterValuePresentation.tsx
147
+ import { createContext, useContext, useState } from "react";
148
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
149
+ var Context = createContext({});
150
+ var FilterValueProvider = Context.Provider;
151
+ function useFilterValuePresentation() {
152
+ return useContext(Context);
153
+ }
154
+ function PresentedFilterValue({ field, value }) {
155
+ const presentation = useContext(Context);
156
+ return /* @__PURE__ */ jsx(Fragment, { children: presentation.render?.(field, value) ?? presentation.label?.(field, value) ?? value });
157
+ }
158
+ function PresentedValueInput({ field, value, options, onChange }) {
159
+ const presentation = useContext(Context);
160
+ const [open, setOpen] = useState(false), [search, setSearch] = useState("");
161
+ const choices = options.map((option) => typeof option === "string" ? { value: option, label: presentation.label?.(field, option) ?? option } : option);
162
+ const visible = choices.filter((option) => `${option.value} ${option.label}`.toLowerCase().includes(search.toLowerCase()));
163
+ return /* @__PURE__ */ jsxs("span", { style: { position: "relative" }, children: [
164
+ /* @__PURE__ */ jsxs("button", { type: "button", "aria-label": `Edit ${field} filter value`, "aria-expanded": open, onClick: () => setOpen(!open), children: [
165
+ /* @__PURE__ */ jsx(PresentedFilterValue, { field, value }),
166
+ !value && "Choose value"
167
+ ] }),
168
+ open && /* @__PURE__ */ jsxs("span", { style: { position: "absolute", top: "100%", left: 0, zIndex: 30, background: "var(--qt-bg, #fff)", padding: 8, minWidth: 220, maxHeight: 300, overflowY: "auto" }, onKeyDown: (e) => {
169
+ if (e.key === "Escape") setOpen(false);
170
+ }, children: [
171
+ /* @__PURE__ */ jsx("input", { autoFocus: true, "aria-label": `Search ${field} values`, value: search, onChange: (e) => setSearch(e.target.value), onKeyDown: (e) => {
172
+ if (e.key === "Enter" && search) {
173
+ e.preventDefault();
174
+ onChange(search);
175
+ setOpen(false);
176
+ }
177
+ } }),
178
+ visible.map((option) => /* @__PURE__ */ jsxs("button", { type: "button", style: { display: "block" }, onClick: () => {
179
+ onChange(option.value);
180
+ setOpen(false);
181
+ }, children: [
182
+ /* @__PURE__ */ jsx(PresentedFilterValue, { field, value: option.value }),
183
+ option.label !== option.value && /* @__PURE__ */ jsxs("small", { children: [
184
+ " ",
185
+ option.value
186
+ ] })
187
+ ] }, option.value)),
188
+ search && !choices.some((option) => option.value === search) && /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => {
189
+ onChange(search);
190
+ setOpen(false);
191
+ }, children: [
192
+ "Use \u201C",
193
+ search,
194
+ "\u201D"
195
+ ] })
196
+ ] })
197
+ ] });
198
+ }
199
+
145
200
  // src/CellMenu.tsx
146
201
  import { useEffect } from "react";
147
- import { NULLARY_OPS, opsForField } from "@pythia-software/query-table-core";
148
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
202
+ import { NULLARY_OPS, negateClause, opsForField, opPairsForField, isFilterable } from "@pythia-software/query-table-core";
203
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
149
204
  var cx = (...parts) => parts.filter(Boolean).join(" ");
150
- var MENU_WIDTH = 240;
205
+ var MENU_WIDTH = 320;
151
206
  var ROW_HEIGHT = 32;
152
207
  var POINTER_OFFSET = 4;
153
208
  function CellMenu({ field, value, x, y, onAddFilter, onClose, classNames }) {
@@ -166,73 +221,36 @@ function CellMenu({ field, value, x, y, onAddFilter, onClose, classNames }) {
166
221
  clearTimeout(t);
167
222
  };
168
223
  }, [onClose]);
169
- const ops = opsForField(field);
224
+ const ops = isFilterable(field) ? opsForField(field) : [];
225
+ const pairs = isFilterable(field) ? opPairsForField(field) : [];
226
+ const hasNullity = pairs.some((p) => p.keep.op === "is_null");
170
227
  const isArray = Array.isArray(value);
171
228
  const isNullish = value == null || value === "" || isArray && value.length === 0;
172
- function apply(op, raw) {
173
- const v = raw != null ? raw : valueToString(value);
174
- onAddFilter({ field: field.name, op, value: NULLARY_OPS.has(op) ? "" : v });
229
+ const cellText = valueToString(value);
230
+ function applyClause(clause) {
231
+ onAddFilter(clause);
175
232
  onClose();
176
233
  }
177
- const filterItems = [];
234
+ const rows = [];
235
+ const pairRow = (op, raw) => {
236
+ const positive = { field: field.name, op, value: NULLARY_OPS.has(op) ? "" : raw };
237
+ rows.push({ positive, negative: negateClause(positive, ops) });
238
+ };
178
239
  if (isArray && !isNullish) {
179
- value.forEach((el, i) => {
180
- const s = String(el);
181
- const arrayOp = ops.includes("includes") ? "includes" : ops[0] ?? "=";
182
- filterItems.push(
183
- /* @__PURE__ */ jsxs(MenuItem, { classNames, onClick: () => apply(arrayOp, s), children: [
184
- "filter: ",
185
- opSymbol(arrayOp),
186
- " ",
187
- /* @__PURE__ */ jsx("code", { children: s })
188
- ] }, `el-${i}`)
189
- );
190
- });
240
+ const arrayOp = pairs.find((p) => p.keep.op === "includes")?.keep.op ?? pairs[0]?.keep.op ?? "=";
241
+ value.forEach((el) => pairRow(arrayOp, String(el)));
242
+ if (hasNullity) pairRow("is_null", cellText);
243
+ } else if (!isNullish) {
244
+ for (const pair of pairs) pairRow(pair.keep.op, cellText);
245
+ } else if (hasNullity) {
246
+ pairRow("is_null", cellText);
191
247
  }
192
- for (const op of ops) {
193
- const nullary = NULLARY_OPS.has(op);
194
- if (isNullish && !nullary) continue;
195
- if (isArray && !nullary && !isNullish) continue;
196
- filterItems.push(
197
- /* @__PURE__ */ jsxs(MenuItem, { classNames, onClick: () => apply(op), children: [
198
- "filter: ",
199
- opSymbol(op),
200
- !nullary && !isNullish ? /* @__PURE__ */ jsxs(Fragment, { children: [
201
- " ",
202
- /* @__PURE__ */ jsx("code", { children: previewValue(value) })
203
- ] }) : null
204
- ] }, op)
205
- );
206
- }
207
- const copyItem = /* @__PURE__ */ jsx(
208
- MenuItem,
209
- {
210
- classNames,
211
- onClick: async () => {
212
- try {
213
- await navigator.clipboard.writeText(valueToString(value));
214
- } catch {
215
- }
216
- onClose();
217
- },
218
- children: "copy value"
219
- },
220
- "copy"
221
- );
222
- const items = [
223
- /* @__PURE__ */ jsxs("div", { className: "qt-cm-header", children: [
224
- /* @__PURE__ */ jsx("span", { className: "qt-cm-field", children: field.label }),
225
- /* @__PURE__ */ jsx("span", { className: "qt-cm-value", children: previewValue(value) })
226
- ] }, "header"),
227
- copyItem,
228
- /* @__PURE__ */ jsx("div", { className: cx("qt-cm-sep", classNames?.separator) }, "sep"),
229
- ...filterItems
230
- ];
231
248
  const vw = typeof window !== "undefined" ? window.innerWidth : 9999;
232
249
  const vh = typeof window !== "undefined" ? window.innerHeight : 9999;
250
+ const approxRows = 3 + rows.length;
233
251
  const left = Math.max(8, Math.min(x + POINTER_OFFSET, vw - MENU_WIDTH - 8));
234
- const top = Math.max(8, Math.min(y + POINTER_OFFSET, vh - ROW_HEIGHT * items.length - 8));
235
- return /* @__PURE__ */ jsx(
252
+ const top = Math.max(8, Math.min(y + POINTER_OFFSET, vh - ROW_HEIGHT * approxRows - 8));
253
+ return /* @__PURE__ */ jsxs2(
236
254
  "div",
237
255
  {
238
256
  className: cx("qt-cell-menu", classNames?.popover),
@@ -240,7 +258,69 @@ function CellMenu({ field, value, x, y, onAddFilter, onClose, classNames }) {
240
258
  onClick: (e) => e.stopPropagation(),
241
259
  onContextMenu: (e) => e.preventDefault(),
242
260
  role: "menu",
243
- children: items
261
+ children: [
262
+ /* @__PURE__ */ jsxs2("div", { className: "qt-cm-header", children: [
263
+ /* @__PURE__ */ jsx2("span", { className: "qt-cm-field", children: field.label }),
264
+ /* @__PURE__ */ jsx2("span", { className: "qt-cm-value", children: Array.isArray(value) ? value.map((v) => /* @__PURE__ */ jsx2(PresentedFilterValue, { field: field.name, value: String(v) }, String(v))) : /* @__PURE__ */ jsx2(PresentedFilterValue, { field: field.name, value: String(value ?? "") }) })
265
+ ] }),
266
+ /* @__PURE__ */ jsx2(
267
+ MenuItem,
268
+ {
269
+ classNames,
270
+ onClick: async () => {
271
+ try {
272
+ await navigator.clipboard.writeText(cellText);
273
+ } catch {
274
+ }
275
+ onClose();
276
+ },
277
+ children: "copy value"
278
+ }
279
+ ),
280
+ /* @__PURE__ */ jsx2("div", { className: cx("qt-cm-sep", classNames?.separator) }),
281
+ rows.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "qt-cm-filter-grid", role: "group", "aria-label": "Filter by this cell", children: [
282
+ /* @__PURE__ */ jsx2("span", { className: "qt-cm-col-head", children: "keep" }),
283
+ /* @__PURE__ */ jsx2("span", { className: "qt-cm-col-head qt-cm-col-head--neg", children: "exclude" }),
284
+ rows.map((row, i) => /* @__PURE__ */ jsx2(FilterPairRow, { row, value, classNames, onApply: applyClause }, i))
285
+ ] })
286
+ ]
287
+ }
288
+ );
289
+ }
290
+ function FilterPairRow({
291
+ row,
292
+ value,
293
+ classNames,
294
+ onApply
295
+ }) {
296
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
297
+ /* @__PURE__ */ jsx2(FilterButton, { clause: row.positive, value, classNames, onApply }),
298
+ /* @__PURE__ */ jsx2(FilterButton, { clause: row.negative, value, negative: true, classNames, onApply })
299
+ ] });
300
+ }
301
+ function FilterButton({
302
+ clause,
303
+ value,
304
+ negative,
305
+ classNames,
306
+ onApply
307
+ }) {
308
+ if (!clause) return /* @__PURE__ */ jsx2("span", { className: "qt-cm-filter-empty", "aria-hidden": true });
309
+ const nullary = NULLARY_OPS.has(clause.op);
310
+ return /* @__PURE__ */ jsxs2(
311
+ "button",
312
+ {
313
+ type: "button",
314
+ className: cx("qt-cm-item", "qt-cm-filter", negative && "qt-cm-filter--neg", classNames?.item),
315
+ onClick: () => onApply(clause),
316
+ role: "menuitem",
317
+ children: [
318
+ /* @__PURE__ */ jsx2("span", { className: "qt-cm-op", children: predicateSymbol(clause) }),
319
+ !nullary ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
320
+ " ",
321
+ /* @__PURE__ */ jsx2("code", { children: /* @__PURE__ */ jsx2(PresentedFilterValue, { field: clause.field, value: clause.value }) })
322
+ ] }) : null
323
+ ]
244
324
  }
245
325
  );
246
326
  }
@@ -249,7 +329,11 @@ function MenuItem({
249
329
  onClick,
250
330
  classNames
251
331
  }) {
252
- return /* @__PURE__ */ jsx("button", { type: "button", className: cx("qt-cm-item", classNames?.item), onClick, role: "menuitem", children });
332
+ return /* @__PURE__ */ jsx2("button", { type: "button", className: cx("qt-cm-item", classNames?.item), onClick, role: "menuitem", children });
333
+ }
334
+ function predicateSymbol(clause) {
335
+ const base = opSymbol(clause.op);
336
+ return clause.negated ? `not ${base}` : base;
253
337
  }
254
338
  function opSymbol(op) {
255
339
  switch (op) {
@@ -259,6 +343,14 @@ function opSymbol(op) {
259
343
  return "\u2265";
260
344
  case "<=":
261
345
  return "\u2264";
346
+ case "starts_with":
347
+ return "starts with";
348
+ case "ends_with":
349
+ return "ends with";
350
+ case "matches_regex":
351
+ return "matches";
352
+ case "not_matches_regex":
353
+ return "not matches";
262
354
  case "is_null":
263
355
  return "is null";
264
356
  case "is_not_null":
@@ -272,17 +364,9 @@ function valueToString(v) {
272
364
  if (Array.isArray(v)) return v.join(", ");
273
365
  return String(v);
274
366
  }
275
- function previewValue(v) {
276
- if (v == null || v === "") return "(empty)";
277
- if (Array.isArray(v)) {
278
- return v.length === 0 ? "(empty)" : `[${v.length}] ${v.slice(0, 2).join(", ")}${v.length > 2 ? "\u2026" : ""}`;
279
- }
280
- const s = String(v);
281
- return s.length > 50 ? s.slice(0, 50) + "\u2026" : s;
282
- }
283
367
 
284
368
  // src/DataTable.tsx
285
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
369
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
286
370
  var cx2 = (...parts) => parts.filter((p) => Boolean(p)).join(" ");
287
371
  var MIN_WIDTH = 1;
288
372
  var MIN_SELECTION_WIDTH = 1;
@@ -316,12 +400,12 @@ function DataTable(props) {
316
400
  overscan = DEFAULT_OVERSCAN,
317
401
  classNames
318
402
  } = props;
319
- const [menu, setMenu] = useState2(null);
320
- const [selectionColumnWidth, setSelectionColumnWidth] = useState2(DEFAULT_SELECTION_WIDTH);
321
- const [tableColumnOrder, setTableColumnOrder] = useState2([]);
322
- const [copiedCellKey, setCopiedCellKey] = useState2(null);
323
- const [headerHeight, setHeaderHeight] = useState2(0);
324
- const [showHorizontalCue, setShowHorizontalCue] = useState2(false);
403
+ const [menu, setMenu] = useState3(null);
404
+ const [selectionColumnWidth, setSelectionColumnWidth] = useState3(DEFAULT_SELECTION_WIDTH);
405
+ const [tableColumnOrder, setTableColumnOrder] = useState3([]);
406
+ const [copiedCellKey, setCopiedCellKey] = useState3(null);
407
+ const [headerHeight, setHeaderHeight] = useState3(0);
408
+ const [showHorizontalCue, setShowHorizontalCue] = useState3(false);
325
409
  const tableWrapRef = useRef(null);
326
410
  const tableHeadRef = useRef(null);
327
411
  const tableRef = useRef(null);
@@ -649,7 +733,7 @@ function DataTable(props) {
649
733
  }
650
734
  }
651
735
  function resizeHandle(name, label, width) {
652
- return /* @__PURE__ */ jsx2(
736
+ return /* @__PURE__ */ jsx3(
653
737
  "span",
654
738
  {
655
739
  className: cx2("qt-resize-handle", classNames?.resizeHandle),
@@ -697,12 +781,12 @@ function DataTable(props) {
697
781
  const lastVirtualRow = virtualRows[virtualRows.length - 1];
698
782
  const paddingTop = firstVirtualRow ? Math.max(0, firstVirtualRow.start - headerHeight) : 0;
699
783
  const paddingBottom = lastVirtualRow ? Math.max(0, rowVirtualizer.getTotalSize() - (lastVirtualRow.end - headerHeight)) : 0;
700
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
701
- showHorizontalCue ? /* @__PURE__ */ jsxs2("div", { className: "qt-table-scroll-cue", "aria-hidden": "true", children: [
784
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
785
+ showHorizontalCue ? /* @__PURE__ */ jsxs3("div", { className: "qt-table-scroll-cue", "aria-hidden": "true", children: [
702
786
  "Swipe to see more ",
703
- /* @__PURE__ */ jsx2("span", { children: "\u2192" })
787
+ /* @__PURE__ */ jsx3("span", { children: "\u2192" })
704
788
  ] }) : null,
705
- /* @__PURE__ */ jsxs2(
789
+ /* @__PURE__ */ jsxs3(
706
790
  "div",
707
791
  {
708
792
  ref: tableWrapRef,
@@ -722,8 +806,8 @@ function DataTable(props) {
722
806
  "aria-label": "Scrollable data table",
723
807
  tabIndex: 0,
724
808
  children: [
725
- loading && /* @__PURE__ */ jsx2("div", { className: cx2("qt-loading-bar", classNames?.loadingBar), role: "progressbar", "aria-label": "loading" }),
726
- /* @__PURE__ */ jsx2(
809
+ loading && /* @__PURE__ */ jsx3("div", { className: cx2("qt-loading-bar", classNames?.loadingBar), role: "progressbar", "aria-label": "loading" }),
810
+ /* @__PURE__ */ jsx3(
727
811
  "div",
728
812
  {
729
813
  ref: resizeGuideRef,
@@ -731,21 +815,21 @@ function DataTable(props) {
731
815
  "aria-hidden": "true"
732
816
  }
733
817
  ),
734
- /* @__PURE__ */ jsxs2("table", { ref: tableRef, className: cx2("qt-table", classNames?.table), style: { minWidth: tableWidth }, children: [
735
- /* @__PURE__ */ jsxs2("colgroup", { children: [
818
+ /* @__PURE__ */ jsxs3("table", { ref: tableRef, className: cx2("qt-table", classNames?.table), style: { minWidth: tableWidth }, children: [
819
+ /* @__PURE__ */ jsxs3("colgroup", { children: [
736
820
  renderedColumnNames.map((name) => {
737
821
  const f = fieldByName.get(name);
738
822
  const w = displayColumnWidthFor(name, f);
739
- if (name === SELECTION_COLUMN) return /* @__PURE__ */ jsx2("col", { "data-qt-column": name, style: { width: w } }, name);
823
+ if (name === SELECTION_COLUMN) return /* @__PURE__ */ jsx3("col", { "data-qt-column": name, style: { width: w } }, name);
740
824
  if (!f) return null;
741
- return /* @__PURE__ */ jsx2("col", { "data-qt-column": f.name, style: { width: w } }, f.name);
825
+ return /* @__PURE__ */ jsx3("col", { "data-qt-column": f.name, style: { width: w } }, f.name);
742
826
  }),
743
- trailing && /* @__PURE__ */ jsx2("col", { style: { width: 40 } })
827
+ trailing && /* @__PURE__ */ jsx3("col", { style: { width: 40 } })
744
828
  ] }),
745
- /* @__PURE__ */ jsx2("thead", { ref: tableHeadRef, className: classNames?.thead, children: /* @__PURE__ */ jsxs2("tr", { className: classNames?.headerRow, children: [
829
+ /* @__PURE__ */ jsx3("thead", { ref: tableHeadRef, className: classNames?.thead, children: /* @__PURE__ */ jsxs3("tr", { className: classNames?.headerRow, children: [
746
830
  renderedColumnNames.map((name) => {
747
831
  if (name === SELECTION_COLUMN) {
748
- return /* @__PURE__ */ jsxs2(
832
+ return /* @__PURE__ */ jsxs3(
749
833
  "th",
750
834
  {
751
835
  "data-qt-field": name,
@@ -789,7 +873,7 @@ function DataTable(props) {
789
873
  },
790
874
  onDragEnd: () => columnDrag.end(),
791
875
  children: [
792
- /* @__PURE__ */ jsx2(
876
+ /* @__PURE__ */ jsx3(
793
877
  "input",
794
878
  {
795
879
  type: "checkbox",
@@ -811,7 +895,7 @@ function DataTable(props) {
811
895
  if (!f) return null;
812
896
  const info = sortInfo(f);
813
897
  const sortable = isSortable(f);
814
- return /* @__PURE__ */ jsxs2(
898
+ return /* @__PURE__ */ jsxs3(
815
899
  "th",
816
900
  {
817
901
  "data-qt-field": f.name,
@@ -855,7 +939,7 @@ function DataTable(props) {
855
939
  },
856
940
  onDragEnd: () => columnDrag.end(),
857
941
  children: [
858
- /* @__PURE__ */ jsxs2(
942
+ /* @__PURE__ */ jsxs3(
859
943
  "span",
860
944
  {
861
945
  className: "qt-th-label",
@@ -864,9 +948,9 @@ function DataTable(props) {
864
948
  title: "Click to open column actions",
865
949
  children: [
866
950
  f.label,
867
- info && /* @__PURE__ */ jsxs2("span", { className: "qt-sort-indicator", children: [
951
+ info && /* @__PURE__ */ jsxs3("span", { className: "qt-sort-indicator", children: [
868
952
  info.dir === "asc" ? " \u2191" : " \u2193",
869
- info.priority != null && /* @__PURE__ */ jsx2("sup", { className: "qt-sort-priority", children: info.priority })
953
+ info.priority != null && /* @__PURE__ */ jsx3("sup", { className: "qt-sort-priority", children: info.priority })
870
954
  ] })
871
955
  ]
872
956
  }
@@ -877,17 +961,17 @@ function DataTable(props) {
877
961
  f.name
878
962
  );
879
963
  }),
880
- trailing && /* @__PURE__ */ jsx2("th", { className: cx2("qt-th", classNames?.th), children: trailingLabel ?? "" })
964
+ trailing && /* @__PURE__ */ jsx3("th", { className: cx2("qt-th", classNames?.th), children: trailingLabel ?? "" })
881
965
  ] }) }),
882
- /* @__PURE__ */ jsxs2("tbody", { className: classNames?.tbody, children: [
883
- rows.length === 0 && /* @__PURE__ */ jsx2("tr", { children: /* @__PURE__ */ jsx2("td", { colSpan: totalCols, className: cx2("qt-cell", "qt-empty", classNames?.empty), children: emptyMessage ?? "No rows." }) }),
884
- paddingTop > 0 && /* @__PURE__ */ jsx2("tr", { className: "qt-virtual-spacer", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("td", { colSpan: totalCols, style: { height: paddingTop, padding: 0, border: 0 } }) }),
966
+ /* @__PURE__ */ jsxs3("tbody", { className: classNames?.tbody, children: [
967
+ rows.length === 0 && /* @__PURE__ */ jsx3("tr", { children: /* @__PURE__ */ jsx3("td", { colSpan: totalCols, className: cx2("qt-cell", "qt-empty", classNames?.empty), children: emptyMessage ?? "No rows." }) }),
968
+ paddingTop > 0 && /* @__PURE__ */ jsx3("tr", { className: "qt-virtual-spacer", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("td", { colSpan: totalCols, style: { height: paddingTop, padding: 0, border: 0 } }) }),
885
969
  virtualRows.map((virtualRow) => {
886
970
  const i = virtualRow.index;
887
971
  const row = rows[i];
888
972
  const id = rowIds[i] ?? null;
889
973
  const selected = id != null && selection ? selection.isSelected(id) : false;
890
- return /* @__PURE__ */ jsxs2(
974
+ return /* @__PURE__ */ jsxs3(
891
975
  "tr",
892
976
  {
893
977
  "data-index": i,
@@ -896,7 +980,7 @@ function DataTable(props) {
896
980
  children: [
897
981
  renderedColumnNames.map((name) => {
898
982
  if (name === SELECTION_COLUMN) {
899
- return /* @__PURE__ */ jsx2(
983
+ return /* @__PURE__ */ jsx3(
900
984
  "td",
901
985
  {
902
986
  "data-qt-field": name,
@@ -907,7 +991,7 @@ function DataTable(props) {
907
991
  classNames?.cell,
908
992
  classNames?.checkboxCell
909
993
  ),
910
- children: id != null && /* @__PURE__ */ jsx2(
994
+ children: id != null && /* @__PURE__ */ jsx3(
911
995
  "input",
912
996
  {
913
997
  type: "checkbox",
@@ -927,7 +1011,7 @@ function DataTable(props) {
927
1011
  const render = resolveRenderer(f, renderers);
928
1012
  const value = readFieldValue(f, row);
929
1013
  const key = cellKey(id, i, f.name);
930
- return /* @__PURE__ */ jsx2(
1014
+ return /* @__PURE__ */ jsx3(
931
1015
  "td",
932
1016
  {
933
1017
  "data-qt-field": f.name,
@@ -936,21 +1020,21 @@ function DataTable(props) {
936
1020
  onClick: (e) => openMenu(e, f, row),
937
1021
  onContextMenu: (e) => openMenu(e, f, row),
938
1022
  onDoubleClick: (e) => copyCellToClipboard(e, f, id, i, value),
939
- children: copiedCellKey === key ? /* @__PURE__ */ jsx2("span", { className: "qt-cell-copy-chip", children: "\u2713 copied" }) : render({ value, row, field: f, query })
1023
+ children: copiedCellKey === key ? /* @__PURE__ */ jsx3("span", { className: "qt-cell-copy-chip", children: "\u2713 copied" }) : isComputedCellError(value) ? /* @__PURE__ */ jsx3("span", { className: "qt-formula-error", title: value.computedError, children: value.computedError === "Calculating\u2026" ? "Calculating\u2026" : "Error" }) : render({ value, row, field: f, query })
940
1024
  },
941
1025
  f.name
942
1026
  );
943
1027
  }),
944
- trailing && /* @__PURE__ */ jsx2("td", { className: cx2("qt-cell", "qt-trailing-cell", classNames?.cell), children: trailing(row) })
1028
+ trailing && /* @__PURE__ */ jsx3("td", { className: cx2("qt-cell", "qt-trailing-cell", classNames?.cell), children: trailing(row) })
945
1029
  ]
946
1030
  },
947
1031
  virtualRow.key
948
1032
  );
949
1033
  }),
950
- paddingBottom > 0 && /* @__PURE__ */ jsx2("tr", { className: "qt-virtual-spacer", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("td", { colSpan: totalCols, style: { height: paddingBottom, padding: 0, border: 0 } }) })
1034
+ paddingBottom > 0 && /* @__PURE__ */ jsx3("tr", { className: "qt-virtual-spacer", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("td", { colSpan: totalCols, style: { height: paddingBottom, padding: 0, border: 0 } }) })
951
1035
  ] }),
952
- /* @__PURE__ */ jsx2("tfoot", { children: /* @__PURE__ */ jsx2("tr", { className: cx2("qt-table-summary-row", classNames?.summaryRow), children: /* @__PURE__ */ jsx2("td", { colSpan: totalCols, className: cx2("qt-cell", "qt-table-summary-cell", classNames?.summaryCell), children: /* @__PURE__ */ jsxs2("div", { className: cx2("qt-table-summary", classNames?.summaryControls), children: [
953
- /* @__PURE__ */ jsx2(
1036
+ /* @__PURE__ */ jsx3("tfoot", { children: /* @__PURE__ */ jsx3("tr", { className: cx2("qt-table-summary-row", classNames?.summaryRow), children: /* @__PURE__ */ jsx3("td", { colSpan: totalCols, className: cx2("qt-cell", "qt-table-summary-cell", classNames?.summaryCell), children: /* @__PURE__ */ jsxs3("div", { className: cx2("qt-table-summary", classNames?.summaryControls), children: [
1037
+ /* @__PURE__ */ jsx3(
954
1038
  "button",
955
1039
  {
956
1040
  type: "button",
@@ -960,8 +1044,8 @@ function DataTable(props) {
960
1044
  children: "\u2190 prev"
961
1045
  }
962
1046
  ),
963
- /* @__PURE__ */ jsx2("span", { className: cx2("qt-table-summary-hint", classNames?.summaryHint), children: summaryText }),
964
- /* @__PURE__ */ jsx2(
1047
+ /* @__PURE__ */ jsx3("span", { className: cx2("qt-table-summary-hint", classNames?.summaryHint), children: summaryText }),
1048
+ /* @__PURE__ */ jsx3(
965
1049
  "button",
966
1050
  {
967
1051
  type: "button",
@@ -976,7 +1060,7 @@ function DataTable(props) {
976
1060
  ]
977
1061
  }
978
1062
  ),
979
- menu && (menu.kind === "cell" ? /* @__PURE__ */ jsx2(
1063
+ menu && (menu.kind === "cell" ? /* @__PURE__ */ jsx3(
980
1064
  CellMenu,
981
1065
  {
982
1066
  field: menu.field,
@@ -986,7 +1070,7 @@ function DataTable(props) {
986
1070
  onAddFilter: (clause) => onQueryChange((prev) => ({ ...prev, offset: 0, where: [...prev.where, clause] })),
987
1071
  onClose: () => setMenu(null)
988
1072
  }
989
- ) : /* @__PURE__ */ jsx2(
1073
+ ) : /* @__PURE__ */ jsx3(
990
1074
  HeaderMenu,
991
1075
  {
992
1076
  field: menu.field,
@@ -1028,7 +1112,7 @@ function HeaderMenu({
1028
1112
  }, [onClose]);
1029
1113
  const items = [];
1030
1114
  const addSortItem = (placement, dir, label) => items.push(
1031
- /* @__PURE__ */ jsx2(MenuItem2, { disabled: !sortable, onClick: () => onSetSort(placement, dir), children: label }, `${placement}-${dir}`)
1115
+ /* @__PURE__ */ jsx3(MenuItem2, { disabled: !sortable, onClick: () => onSetSort(placement, dir), children: label }, `${placement}-${dir}`)
1032
1116
  );
1033
1117
  addSortItem("set", "asc", "Set Sort (Asc)");
1034
1118
  addSortItem("set", "desc", "Set Sort (Desc)");
@@ -1036,15 +1120,15 @@ function HeaderMenu({
1036
1120
  addSortItem("append", "desc", "Append Sort (Desc)");
1037
1121
  addSortItem("prepend", "asc", "Prepend Sort (Asc)");
1038
1122
  addSortItem("prepend", "desc", "Prepend Sort (Desc)");
1039
- items.push(/* @__PURE__ */ jsx2("div", { className: "qt-cm-sep" }, "sep"));
1123
+ items.push(/* @__PURE__ */ jsx3("div", { className: "qt-cm-sep" }, "sep"));
1040
1124
  items.push(
1041
- /* @__PURE__ */ jsx2(MenuItem2, { disabled: !canRemoveColumn, onClick: onRemove, children: "Remove Column" }, "remove")
1125
+ /* @__PURE__ */ jsx3(MenuItem2, { disabled: !canRemoveColumn, onClick: onRemove, children: "Remove Column" }, "remove")
1042
1126
  );
1043
1127
  const vw = typeof window !== "undefined" ? window.innerWidth : 9999;
1044
1128
  const vh = typeof window !== "undefined" ? window.innerHeight : 9999;
1045
1129
  const left = Math.max(8, Math.min(x, vw - MENU_WIDTH2 - 8));
1046
1130
  const top = Math.max(8, Math.min(y, vh - MENU_ROW_HEIGHT * items.length - 8));
1047
- return /* @__PURE__ */ jsxs2(
1131
+ return /* @__PURE__ */ jsxs3(
1048
1132
  "div",
1049
1133
  {
1050
1134
  className: "qt-cell-menu",
@@ -1053,7 +1137,7 @@ function HeaderMenu({
1053
1137
  onContextMenu: (e) => e.preventDefault(),
1054
1138
  role: "menu",
1055
1139
  children: [
1056
- /* @__PURE__ */ jsx2("div", { className: "qt-cm-header", children: /* @__PURE__ */ jsx2("span", { className: "qt-cm-field", children: field.label }) }),
1140
+ /* @__PURE__ */ jsx3("div", { className: "qt-cm-header", children: /* @__PURE__ */ jsx3("span", { className: "qt-cm-field", children: field.label }) }),
1057
1141
  items
1058
1142
  ]
1059
1143
  }
@@ -1064,7 +1148,7 @@ function MenuItem2({
1064
1148
  onClick,
1065
1149
  disabled
1066
1150
  }) {
1067
- return /* @__PURE__ */ jsx2(
1151
+ return /* @__PURE__ */ jsx3(
1068
1152
  "button",
1069
1153
  {
1070
1154
  type: "button",
@@ -1114,8 +1198,814 @@ function valueToString2(value) {
1114
1198
  return String(value);
1115
1199
  }
1116
1200
 
1201
+ // src/SelectColumnEditor.tsx
1202
+ import {
1203
+ lazy,
1204
+ Suspense,
1205
+ useEffect as useEffect3,
1206
+ useId,
1207
+ useMemo as useMemo2,
1208
+ useRef as useRef2,
1209
+ useState as useState4
1210
+ } from "react";
1211
+ import {
1212
+ computedFieldName,
1213
+ FORMULA_FUNCTIONS,
1214
+ isComputedField,
1215
+ isSelectable
1216
+ } from "@pythia-software/query-table-core";
1217
+
1218
+ // src/previewOrdering.ts
1219
+ var collator = new Intl.Collator(void 0, {
1220
+ numeric: true,
1221
+ sensitivity: "base"
1222
+ });
1223
+ var valueLabel = (value) => value === null ? "NULL" : typeof value === "string" ? value : JSON.stringify(value);
1224
+ function mixedRank(group) {
1225
+ const value = JSON.stringify([
1226
+ group.inputs,
1227
+ group.result.value,
1228
+ group.result.error ?? null
1229
+ ]);
1230
+ let hash = 2166136261;
1231
+ for (let i = 0; i < value.length; i++)
1232
+ hash = Math.imul(hash ^ value.charCodeAt(i), 16777619);
1233
+ return hash >>> 0;
1234
+ }
1235
+ function compareValue(a, b) {
1236
+ return collator.compare(valueLabel(a), valueLabel(b));
1237
+ }
1238
+ function sortValue(group, key) {
1239
+ switch (key.kind) {
1240
+ case "input":
1241
+ return group.inputs[key.index] ?? null;
1242
+ case "result":
1243
+ return group.result.error ?? group.result.value;
1244
+ case "count":
1245
+ case "percentage":
1246
+ return group.count;
1247
+ }
1248
+ }
1249
+ function orderPreviewGroups(groups, sort) {
1250
+ return groups.map((group, index) => ({ group, index })).sort((a, b) => {
1251
+ if (!sort) {
1252
+ const rank = mixedRank(a.group) - mixedRank(b.group);
1253
+ return rank || a.index - b.index;
1254
+ }
1255
+ const comparison = compareValue(
1256
+ sortValue(a.group, sort.key),
1257
+ sortValue(b.group, sort.key)
1258
+ );
1259
+ return (sort.direction === "asc" ? comparison : -comparison) || a.index - b.index;
1260
+ }).map(({ group }) => group);
1261
+ }
1262
+
1263
+ // src/SelectColumnEditor.tsx
1264
+ import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1265
+ var FormulaEditor = lazy(() => import("./FormulaEditor-YCPICDIH.js"));
1266
+ var fieldExpression = (name) => `[${name.replace(/\]/g, "]]")}]`;
1267
+ var display = (v) => v === null ? "NULL" : typeof v === "string" ? v === "" ? '""' : v : JSON.stringify(v);
1268
+ function SelectColumnEditor({
1269
+ api,
1270
+ onClose
1271
+ }) {
1272
+ const titleId = useId(), dialog = useRef2(null);
1273
+ const closeRef = useRef2(onClose);
1274
+ closeRef.current = onClose;
1275
+ const [columns, setColumns] = useState4(
1276
+ () => api.select.visible.map((c) => ({ ...c }))
1277
+ );
1278
+ const [search, setSearch] = useState4(""), [active, setActive] = useState4(api.select.visible[0]?.field ?? "");
1279
+ const [editing, setEditing] = useState4(false), [id, setId] = useState4(""), [revision, setRevision] = useState4(null);
1280
+ const [label, setLabel] = useState4(""), [source, setSource] = useState4("");
1281
+ const [count, setCount] = useState4(1e3), [preview, setPreview] = useState4(null), [loading, setLoading] = useState4(false);
1282
+ const [error, setError] = useState4(null), [notice, setNotice] = useState4(""), [saving, setSaving] = useState4(false);
1283
+ const [filter, setFilter] = useState4("all"), [page, setPage] = useState4(0), [retry, setRetry] = useState4(0), [functionSearch, setFunctionSearch] = useState4("");
1284
+ const [previewSort, setPreviewSort] = useState4(null);
1285
+ const [fieldStats, setFieldStats] = useState4({});
1286
+ const drag = useRef2(null);
1287
+ const [inspection, setInspection] = useState4(null);
1288
+ const catalogue = api.computed.catalogue.filter(isSelectable);
1289
+ const chosen = catalogue.find((f) => f.name === active);
1290
+ const expression = editing ? source : active ? fieldExpression(active) : "";
1291
+ const validation = useMemo2(() => {
1292
+ if (!expression)
1293
+ return { error: "Choose a column or create a computed column." };
1294
+ try {
1295
+ return {
1296
+ plan: api.computed.compile(expression, editing ? id : void 0)
1297
+ };
1298
+ } catch (e) {
1299
+ return { error: e instanceof Error ? e.message : String(e) };
1300
+ }
1301
+ }, [expression, editing, id, api.computed.compile]);
1302
+ const previewPlanKey = validation.error ? `error:${validation.error}` : `plan:${JSON.stringify(validation.plan)}`;
1303
+ const previewRequest = useRef2(api.computed.preview);
1304
+ previewRequest.current = api.computed.preview;
1305
+ const fieldStatsRequest = useRef2(api.fieldStats);
1306
+ fieldStatsRequest.current = api.fieldStats;
1307
+ const catalogueKey = JSON.stringify(catalogue.map((field) => field.name));
1308
+ useEffect3(() => {
1309
+ const previous = document.activeElement;
1310
+ dialog.current?.focus();
1311
+ const onKey = (e) => {
1312
+ if (e.key === "Escape") {
1313
+ if (document.querySelector(".cm-tooltip-autocomplete")) return;
1314
+ closeRef.current();
1315
+ }
1316
+ if (e.key === "Tab") {
1317
+ const focusable = Array.from(
1318
+ dialog.current?.querySelectorAll(
1319
+ 'button:not([disabled]), input:not([disabled]), select, textarea, [tabindex="0"], [contenteditable="true"]'
1320
+ ) ?? []
1321
+ ).filter((el) => el.getClientRects().length);
1322
+ const first = focusable[0], last = focusable[focusable.length - 1];
1323
+ if (e.shiftKey && (document.activeElement === first || document.activeElement === dialog.current)) {
1324
+ e.preventDefault();
1325
+ last?.focus();
1326
+ } else if (!e.shiftKey && document.activeElement === last) {
1327
+ e.preventDefault();
1328
+ first?.focus();
1329
+ }
1330
+ }
1331
+ };
1332
+ document.addEventListener("keydown", onKey);
1333
+ const old = document.body.style.overflow;
1334
+ document.body.style.overflow = "hidden";
1335
+ return () => {
1336
+ document.removeEventListener("keydown", onKey);
1337
+ document.body.style.overflow = old;
1338
+ previous?.focus();
1339
+ };
1340
+ }, []);
1341
+ useEffect3(() => {
1342
+ const ac = new AbortController();
1343
+ void fieldStatsRequest.current(
1344
+ catalogue.map((field) => field.name),
1345
+ ac.signal
1346
+ ).then((result) => {
1347
+ if (!ac.signal.aborted) setFieldStats(result);
1348
+ }).catch(() => {
1349
+ });
1350
+ return () => ac.abort();
1351
+ }, [catalogueKey]);
1352
+ useEffect3(() => {
1353
+ const ac = new AbortController();
1354
+ setPreview(null);
1355
+ setError(null);
1356
+ setPage(0);
1357
+ setInspection(null);
1358
+ if (validation.error || !Number.isInteger(count) || count < 1 || count > 1e4) {
1359
+ setLoading(false);
1360
+ return;
1361
+ }
1362
+ setLoading(true);
1363
+ const timer = setTimeout(() => {
1364
+ const request = previewRequest.current;
1365
+ void request(
1366
+ expression,
1367
+ count,
1368
+ ac.signal,
1369
+ editing ? id : void 0,
1370
+ editing
1371
+ ).then((result) => {
1372
+ if (!ac.signal.aborted) setPreview(result);
1373
+ }).catch((e) => {
1374
+ if (!ac.signal.aborted)
1375
+ setError(e instanceof Error ? e.message : String(e));
1376
+ }).finally(() => {
1377
+ if (!ac.signal.aborted) setLoading(false);
1378
+ });
1379
+ }, 400);
1380
+ return () => {
1381
+ ac.abort();
1382
+ clearTimeout(timer);
1383
+ };
1384
+ }, [expression, count, editing, id, previewPlanKey, retry]);
1385
+ const move = (field, index) => setColumns((cols) => {
1386
+ const next = cols.filter((c) => c.field !== field), item = cols.find((c) => c.field === field);
1387
+ if (item) next.splice(Math.max(0, Math.min(index, next.length)), 0, item);
1388
+ return next;
1389
+ });
1390
+ const toggle = (field) => setColumns(
1391
+ (cols) => cols.some((c) => c.field === field) ? cols.filter((c) => c.field !== field) : [...cols, { field }]
1392
+ );
1393
+ function browse(field) {
1394
+ setActive(field);
1395
+ setEditing(false);
1396
+ setNotice("");
1397
+ }
1398
+ function edit(defId) {
1399
+ const def = api.computed.definitions.find((d) => d.id === defId);
1400
+ setId(def?.id ?? defId ?? crypto.randomUUID());
1401
+ setRevision(def?.revision ?? null);
1402
+ setLabel(def?.label ?? defId ?? "");
1403
+ setSource(
1404
+ def?.expression.source ?? (defId ? "" : active ? fieldExpression(active) : "LEFT([field], 3)")
1405
+ );
1406
+ setEditing(true);
1407
+ setNotice("");
1408
+ }
1409
+ async function save() {
1410
+ setSaving(true);
1411
+ setError(null);
1412
+ try {
1413
+ const def = await api.computed.save(
1414
+ {
1415
+ id,
1416
+ label: label.trim(),
1417
+ expression: { language: "qt-expr", version: 1, source }
1418
+ },
1419
+ revision
1420
+ );
1421
+ setRevision(def.revision);
1422
+ const field = computedFieldName(def.id);
1423
+ setActive(field);
1424
+ setColumns(
1425
+ (cols) => cols.some((c) => c.field === field) ? cols : [...cols, { field }]
1426
+ );
1427
+ setNotice(
1428
+ "Definition saved. All queries referencing this ID now use this definition. Apply columns to update this query\u2019s layout."
1429
+ );
1430
+ } catch (e) {
1431
+ setError(e instanceof Error ? e.message : String(e));
1432
+ } finally {
1433
+ setSaving(false);
1434
+ }
1435
+ }
1436
+ const groups = useMemo2(
1437
+ () => orderPreviewGroups(
1438
+ (preview?.groups ?? []).filter(
1439
+ (g) => filter === "errors" ? !!g.result.error : filter === "nulls" ? !g.result.error && g.result.value === null : true
1440
+ ),
1441
+ previewSort
1442
+ ),
1443
+ [preview, filter, previewSort]
1444
+ );
1445
+ const catalogueResults = useMemo2(
1446
+ () => catalogue.filter(
1447
+ (f) => `${f.label} ${f.name} ${f.type} ${f.group ?? ""}`.toLowerCase().includes(search.toLowerCase())
1448
+ ).map((field, index) => ({ field, index })).sort((a, b) => {
1449
+ const aLow = (fieldStats[a.field.name]?.distinct ?? 2) <= 1;
1450
+ const bLow = (fieldStats[b.field.name]?.distinct ?? 2) <= 1;
1451
+ return Number(aLow) - Number(bLow) || a.index - b.index;
1452
+ }).map(({ field }) => field),
1453
+ [catalogue, search, fieldStats]
1454
+ );
1455
+ const shown = groups.slice(page * 50, (page + 1) * 50);
1456
+ const samePreviewSortKey = (a, b) => {
1457
+ if (a.kind !== b.kind) return false;
1458
+ if (a.kind === "input" && b.kind === "input")
1459
+ return a.index === b.index;
1460
+ return true;
1461
+ };
1462
+ const selectPreviewSort = (key, direction) => {
1463
+ setPreviewSort((current) => {
1464
+ const sameKey = !!current && samePreviewSortKey(current.key, key);
1465
+ return sameKey ? { key, direction: current.direction === "asc" ? "desc" : "asc" } : { key, direction };
1466
+ });
1467
+ setPage(0);
1468
+ };
1469
+ const previewSortDirection = (key) => previewSort && samePreviewSortKey(previewSort.key, key) ? previewSort.direction : void 0;
1470
+ const previewAriaSort = (key) => {
1471
+ const direction = previewSortDirection(key);
1472
+ return direction === "asc" ? "ascending" : direction === "desc" ? "descending" : "none";
1473
+ };
1474
+ const previewSortHeader = (label2, key, direction = "asc") => {
1475
+ const currentDirection = previewSortDirection(key);
1476
+ return /* @__PURE__ */ jsxs4(
1477
+ "button",
1478
+ {
1479
+ type: "button",
1480
+ className: "qt-preview-sort",
1481
+ onClick: () => selectPreviewSort(key, direction),
1482
+ title: `Sort by ${label2}`,
1483
+ children: [
1484
+ label2,
1485
+ currentDirection && (currentDirection === "asc" ? " \u2191" : " \u2193")
1486
+ ]
1487
+ }
1488
+ );
1489
+ };
1490
+ return /* @__PURE__ */ jsx4("div", { className: "qt-modal-backdrop qt-select-editor-backdrop", children: /* @__PURE__ */ jsxs4(
1491
+ "div",
1492
+ {
1493
+ className: "qt-select-editor",
1494
+ role: "dialog",
1495
+ "aria-modal": "true",
1496
+ "aria-labelledby": titleId,
1497
+ ref: dialog,
1498
+ tabIndex: -1,
1499
+ children: [
1500
+ /* @__PURE__ */ jsxs4("header", { className: "qt-select-editor-header", children: [
1501
+ /* @__PURE__ */ jsxs4("div", { children: [
1502
+ /* @__PURE__ */ jsx4("h2", { id: titleId, children: "Select columns" }),
1503
+ /* @__PURE__ */ jsx4("p", { children: "Arrange your view, explore values, and create reusable computed columns." })
1504
+ ] }),
1505
+ /* @__PURE__ */ jsx4(
1506
+ "button",
1507
+ {
1508
+ type: "button",
1509
+ className: "qt-btn",
1510
+ onClick: onClose,
1511
+ "aria-label": "Close column editor",
1512
+ children: "\u2715"
1513
+ }
1514
+ )
1515
+ ] }),
1516
+ /* @__PURE__ */ jsxs4("div", { className: "qt-select-editor-body", children: [
1517
+ /* @__PURE__ */ jsxs4("aside", { className: "qt-selected-panel", children: [
1518
+ /* @__PURE__ */ jsxs4("h3", { children: [
1519
+ "Selected ",
1520
+ /* @__PURE__ */ jsx4("span", { className: "qt-muted", children: columns.length })
1521
+ ] }),
1522
+ /* @__PURE__ */ jsx4("p", { className: "qt-muted", children: "Drag to reorder, or use the arrow buttons." }),
1523
+ /* @__PURE__ */ jsx4("ol", { className: "qt-editor-columns", children: columns.map((c, i) => {
1524
+ const f = catalogue.find((f2) => f2.name === c.field);
1525
+ return /* @__PURE__ */ jsxs4(
1526
+ "li",
1527
+ {
1528
+ draggable: true,
1529
+ onDragStart: (e) => {
1530
+ drag.current = c.field;
1531
+ e.dataTransfer.setData("text/plain", c.field);
1532
+ },
1533
+ onDragOver: (e) => e.preventDefault(),
1534
+ onDrop: (e) => {
1535
+ e.preventDefault();
1536
+ if (drag.current) move(drag.current, i);
1537
+ drag.current = null;
1538
+ },
1539
+ onDragEnd: () => {
1540
+ drag.current = null;
1541
+ },
1542
+ children: [
1543
+ /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", children: "\u283F" }),
1544
+ /* @__PURE__ */ jsx4(
1545
+ "button",
1546
+ {
1547
+ type: "button",
1548
+ className: "qt-column-name",
1549
+ onClick: () => browse(c.field),
1550
+ children: f?.label ?? c.field
1551
+ }
1552
+ ),
1553
+ /* @__PURE__ */ jsx4(
1554
+ "button",
1555
+ {
1556
+ type: "button",
1557
+ className: "qt-icon-btn",
1558
+ disabled: i === 0,
1559
+ onClick: () => move(c.field, i - 1),
1560
+ "aria-label": `Move ${f?.label ?? c.field} up`,
1561
+ children: "\u2191"
1562
+ }
1563
+ ),
1564
+ /* @__PURE__ */ jsx4(
1565
+ "button",
1566
+ {
1567
+ type: "button",
1568
+ className: "qt-icon-btn",
1569
+ disabled: i === columns.length - 1,
1570
+ onClick: () => move(c.field, i + 1),
1571
+ "aria-label": `Move ${f?.label ?? c.field} down`,
1572
+ children: "\u2193"
1573
+ }
1574
+ ),
1575
+ /* @__PURE__ */ jsx4(
1576
+ "button",
1577
+ {
1578
+ type: "button",
1579
+ className: "qt-icon-btn",
1580
+ onClick: () => toggle(c.field),
1581
+ "aria-label": `Remove ${f?.label ?? c.field}`,
1582
+ children: "\xD7"
1583
+ }
1584
+ )
1585
+ ]
1586
+ },
1587
+ c.field
1588
+ );
1589
+ }) }),
1590
+ /* @__PURE__ */ jsx4(
1591
+ "button",
1592
+ {
1593
+ type: "button",
1594
+ className: "qt-link-btn",
1595
+ onClick: () => setColumns(
1596
+ api.defaults.select.length ? api.defaults.select : catalogue.filter((f) => f.select?.default).map((f) => ({ field: f.name }))
1597
+ ),
1598
+ children: "Use default columns"
1599
+ }
1600
+ )
1601
+ ] }),
1602
+ /* @__PURE__ */ jsxs4("aside", { className: "qt-catalogue-panel", children: [
1603
+ /* @__PURE__ */ jsx4("h3", { children: "Column catalogue" }),
1604
+ /* @__PURE__ */ jsx4(
1605
+ "input",
1606
+ {
1607
+ className: "qt-input",
1608
+ "aria-label": "Search columns",
1609
+ placeholder: "Search name or type\u2026",
1610
+ value: search,
1611
+ onChange: (e) => setSearch(e.target.value)
1612
+ }
1613
+ ),
1614
+ /* @__PURE__ */ jsx4(
1615
+ "button",
1616
+ {
1617
+ type: "button",
1618
+ className: "qt-btn qt-create-computed",
1619
+ onClick: () => edit(),
1620
+ children: "\uFF0B Computed column"
1621
+ }
1622
+ ),
1623
+ api.computed.loading && /* @__PURE__ */ jsx4("p", { role: "status", children: "Loading definitions\u2026" }),
1624
+ api.computed.error && /* @__PURE__ */ jsx4("p", { role: "alert", children: api.computed.error }),
1625
+ /* @__PURE__ */ jsx4("div", { className: "qt-catalogue-list", children: catalogueResults.map((f) => {
1626
+ const distinct = fieldStats[f.name]?.distinct;
1627
+ return /* @__PURE__ */ jsxs4(
1628
+ "div",
1629
+ {
1630
+ className: `qt-catalogue-item ${active === f.name && !editing ? "is-active" : ""} ${distinct != null && distinct <= 1 ? "is-low-cardinality" : ""}`,
1631
+ children: [
1632
+ /* @__PURE__ */ jsx4(
1633
+ "input",
1634
+ {
1635
+ type: "checkbox",
1636
+ "aria-label": `Include ${f.label}`,
1637
+ checked: columns.some((c) => c.field === f.name),
1638
+ onChange: () => toggle(f.name)
1639
+ }
1640
+ ),
1641
+ /* @__PURE__ */ jsxs4("button", { type: "button", onClick: () => browse(f.name), children: [
1642
+ /* @__PURE__ */ jsx4("strong", { children: f.label }),
1643
+ /* @__PURE__ */ jsxs4("small", { children: [
1644
+ isComputedField(f.name) ? "\u0192 \xB7 " : "",
1645
+ f.type,
1646
+ " \xB7 ",
1647
+ f.group ?? f.name,
1648
+ distinct != null && ` \xB7 ${distinct.toLocaleString()} distinct`
1649
+ ] })
1650
+ ] })
1651
+ ]
1652
+ },
1653
+ f.name
1654
+ );
1655
+ }) }),
1656
+ /* @__PURE__ */ jsx4(
1657
+ "button",
1658
+ {
1659
+ type: "button",
1660
+ className: "qt-link-btn",
1661
+ onClick: () => void api.computed.reload(),
1662
+ children: "Reload shared definitions"
1663
+ }
1664
+ )
1665
+ ] }),
1666
+ /* @__PURE__ */ jsxs4("section", { className: "qt-column-workbench", children: [
1667
+ editing ? /* @__PURE__ */ jsxs4(Fragment4, { children: [
1668
+ /* @__PURE__ */ jsxs4("div", { className: "qt-editor-title", children: [
1669
+ /* @__PURE__ */ jsx4("h3", { children: revision ? "Edit shared computed column" : "New computed column" }),
1670
+ /* @__PURE__ */ jsx4("span", { className: "qt-type-badge", children: validation.plan?.type ?? "Check formula" })
1671
+ ] }),
1672
+ /* @__PURE__ */ jsxs4("label", { className: "qt-editor-label", children: [
1673
+ "Column name",
1674
+ /* @__PURE__ */ jsx4(
1675
+ "input",
1676
+ {
1677
+ className: "qt-input",
1678
+ value: label,
1679
+ maxLength: 200,
1680
+ onChange: (e) => setLabel(e.target.value),
1681
+ placeholder: "e.g. Email domain"
1682
+ }
1683
+ )
1684
+ ] }),
1685
+ /* @__PURE__ */ jsx4(
1686
+ Suspense,
1687
+ {
1688
+ fallback: /* @__PURE__ */ jsx4(
1689
+ "textarea",
1690
+ {
1691
+ "aria-label": "Computed column formula",
1692
+ value: source,
1693
+ onChange: (e) => setSource(e.target.value)
1694
+ }
1695
+ ),
1696
+ children: /* @__PURE__ */ jsx4(
1697
+ FormulaEditor,
1698
+ {
1699
+ value: source,
1700
+ onChange: setSource,
1701
+ fields: api.computed.catalogue.filter(
1702
+ (f) => f.name !== computedFieldName(id)
1703
+ ),
1704
+ compile: (value) => api.computed.compile(value, id)
1705
+ }
1706
+ )
1707
+ }
1708
+ ),
1709
+ /* @__PURE__ */ jsxs4("details", { className: "qt-function-library", children: [
1710
+ /* @__PURE__ */ jsx4("summary", { children: "Functions & examples" }),
1711
+ /* @__PURE__ */ jsx4(
1712
+ "input",
1713
+ {
1714
+ className: "qt-input",
1715
+ "aria-label": "Search functions",
1716
+ placeholder: "Search functions\u2026",
1717
+ value: functionSearch,
1718
+ onChange: (e) => setFunctionSearch(e.target.value)
1719
+ }
1720
+ ),
1721
+ /* @__PURE__ */ jsx4("div", { className: "qt-function-list", children: FORMULA_FUNCTIONS.filter(
1722
+ (f) => `${f.name} ${f.description}`.toLowerCase().includes(functionSearch.toLowerCase())
1723
+ ).map((f) => /* @__PURE__ */ jsxs4(
1724
+ "button",
1725
+ {
1726
+ type: "button",
1727
+ title: f.description,
1728
+ onClick: () => setSource(`${f.name}(${source})`),
1729
+ children: [
1730
+ /* @__PURE__ */ jsx4("code", { children: f.signature }),
1731
+ /* @__PURE__ */ jsx4("small", { children: f.description })
1732
+ ]
1733
+ },
1734
+ f.name
1735
+ )) }),
1736
+ /* @__PURE__ */ jsxs4("p", { children: [
1737
+ "Examples: ",
1738
+ /* @__PURE__ */ jsx4("code", { children: "LEFT([name], 3)" }),
1739
+ " \xB7",
1740
+ " ",
1741
+ /* @__PURE__ */ jsx4("code", { children: 'IF([amount] > 100, "High", "Low")' }),
1742
+ " \xB7",
1743
+ " ",
1744
+ /* @__PURE__ */ jsx4("code", { children: 'REGEX_EXTRACT([email], "@(.+)$", 1)' })
1745
+ ] })
1746
+ ] }),
1747
+ /* @__PURE__ */ jsxs4("div", { className: "qt-definition-actions", children: [
1748
+ /* @__PURE__ */ jsx4(
1749
+ "button",
1750
+ {
1751
+ type: "button",
1752
+ className: "qt-btn",
1753
+ disabled: saving || !!validation.error || !label.trim(),
1754
+ onClick: () => void save(),
1755
+ children: saving ? "Saving\u2026" : revision ? "Save shared definition" : "Save definition & add column"
1756
+ }
1757
+ ),
1758
+ revision && /* @__PURE__ */ jsx4(
1759
+ "button",
1760
+ {
1761
+ type: "button",
1762
+ className: "qt-link-btn",
1763
+ onClick: () => edit(id),
1764
+ children: "Reload editor from catalogue"
1765
+ }
1766
+ )
1767
+ ] }),
1768
+ /* @__PURE__ */ jsx4("p", { className: "qt-muted", children: "Saving changes this definition across all queries. Cancel below only discards column layout changes." })
1769
+ ] }) : /* @__PURE__ */ jsxs4("div", { className: "qt-editor-title", children: [
1770
+ /* @__PURE__ */ jsxs4("div", { children: [
1771
+ /* @__PURE__ */ jsx4("h3", { children: chosen?.label ?? "Explore a column" }),
1772
+ /* @__PURE__ */ jsxs4("p", { className: "qt-muted", children: [
1773
+ chosen?.name,
1774
+ " ",
1775
+ chosen ? `\xB7 ${chosen.type}` : ""
1776
+ ] })
1777
+ ] }),
1778
+ chosen && isComputedField(chosen.name) && /* @__PURE__ */ jsxs4(Fragment4, { children: [
1779
+ /* @__PURE__ */ jsx4(
1780
+ "button",
1781
+ {
1782
+ type: "button",
1783
+ className: "qt-btn",
1784
+ onClick: () => edit(chosen.name.slice(10)),
1785
+ children: "Edit definition"
1786
+ }
1787
+ ),
1788
+ /* @__PURE__ */ jsx4(
1789
+ "button",
1790
+ {
1791
+ type: "button",
1792
+ className: "qt-btn",
1793
+ onClick: () => {
1794
+ const def = api.computed.definitions.find(
1795
+ (d) => computedFieldName(d.id) === chosen.name
1796
+ );
1797
+ edit();
1798
+ if (def) {
1799
+ setLabel(`${def.label} copy`);
1800
+ setSource(def.expression.source);
1801
+ }
1802
+ },
1803
+ children: "Duplicate"
1804
+ }
1805
+ )
1806
+ ] })
1807
+ ] }),
1808
+ /* @__PURE__ */ jsxs4("div", { className: "qt-preview-controls", children: [
1809
+ /* @__PURE__ */ jsxs4("label", { children: [
1810
+ "Rows to process",
1811
+ " ",
1812
+ /* @__PURE__ */ jsx4(
1813
+ "input",
1814
+ {
1815
+ className: "qt-input",
1816
+ type: "number",
1817
+ min: 1,
1818
+ max: 1e4,
1819
+ step: 1,
1820
+ value: Number.isNaN(count) ? "" : count,
1821
+ onChange: (e) => setCount(e.target.valueAsNumber)
1822
+ }
1823
+ )
1824
+ ] }),
1825
+ /* @__PURE__ */ jsx4(
1826
+ "button",
1827
+ {
1828
+ type: "button",
1829
+ className: "qt-btn",
1830
+ onClick: () => setRetry((n) => n + 1),
1831
+ children: "Refresh preview"
1832
+ }
1833
+ ),
1834
+ /* @__PURE__ */ jsxs4("label", { children: [
1835
+ "Show",
1836
+ " ",
1837
+ /* @__PURE__ */ jsxs4(
1838
+ "select",
1839
+ {
1840
+ value: filter,
1841
+ onChange: (e) => {
1842
+ setFilter(e.target.value);
1843
+ setPage(0);
1844
+ },
1845
+ children: [
1846
+ /* @__PURE__ */ jsx4("option", { value: "all", children: "All results" }),
1847
+ /* @__PURE__ */ jsx4("option", { value: "nulls", children: "Null results" }),
1848
+ /* @__PURE__ */ jsx4("option", { value: "errors", children: "Errors" })
1849
+ ]
1850
+ }
1851
+ )
1852
+ ] })
1853
+ ] }),
1854
+ /* @__PURE__ */ jsx4("p", { className: "qt-muted", children: "First matching rows in current sort order, starting at row 1. Counts describe the processed sample. Maximum 10,000 rows." }),
1855
+ (!Number.isInteger(count) || count < 1 || count > 1e4) && /* @__PURE__ */ jsx4("p", { role: "alert", children: "Choose a whole number between 1 and 10,000." }),
1856
+ validation.error && /* @__PURE__ */ jsx4("p", { className: "qt-formula-error", role: "alert", children: validation.error }),
1857
+ error && /* @__PURE__ */ jsx4("p", { className: "qt-formula-error", role: "alert", children: error }),
1858
+ notice && /* @__PURE__ */ jsx4("p", { className: "qt-editor-notice", role: "status", children: notice }),
1859
+ loading && /* @__PURE__ */ jsx4("p", { role: "status", children: "Processing preview\u2026" }),
1860
+ preview && /* @__PURE__ */ jsxs4(Fragment4, { children: [
1861
+ /* @__PURE__ */ jsxs4("p", { className: "qt-preview-summary", role: "status", children: [
1862
+ /* @__PURE__ */ jsx4("strong", { children: preview.processed.toLocaleString() }),
1863
+ " of",
1864
+ " ",
1865
+ preview.total.toLocaleString(),
1866
+ " matching rows \xB7",
1867
+ " ",
1868
+ /* @__PURE__ */ jsx4("strong", { children: preview.groups.length.toLocaleString() }),
1869
+ " ",
1870
+ "distinct combinations \xB7 ",
1871
+ preview.nulls.toLocaleString(),
1872
+ " null \xB7 ",
1873
+ preview.errors.toLocaleString(),
1874
+ " errors",
1875
+ preview.processed === preview.total ? " \xB7 complete filtered set" : " \xB7 sample"
1876
+ ] }),
1877
+ /* @__PURE__ */ jsx4("div", { className: "qt-preview-table-wrap", children: /* @__PURE__ */ jsxs4("table", { className: "qt-preview-table", children: [
1878
+ /* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
1879
+ editing && preview.dependencies.map((name, index) => /* @__PURE__ */ jsx4(
1880
+ "th",
1881
+ {
1882
+ "aria-sort": previewAriaSort({
1883
+ kind: "input",
1884
+ index
1885
+ }),
1886
+ children: previewSortHeader(
1887
+ api.computed.catalogue.find(
1888
+ (f) => f.name === name
1889
+ )?.label ?? name,
1890
+ { kind: "input", index }
1891
+ )
1892
+ },
1893
+ name
1894
+ )),
1895
+ /* @__PURE__ */ jsx4("th", { "aria-sort": previewAriaSort({ kind: "result" }), children: previewSortHeader(editing ? "Result" : "Value", {
1896
+ kind: "result"
1897
+ }) }),
1898
+ /* @__PURE__ */ jsx4("th", { "aria-sort": previewAriaSort({ kind: "count" }), children: previewSortHeader("Count", { kind: "count" }, "desc") }),
1899
+ /* @__PURE__ */ jsx4("th", { "aria-sort": previewAriaSort({ kind: "percentage" }), children: previewSortHeader("%", { kind: "percentage" }, "desc") })
1900
+ ] }) }),
1901
+ /* @__PURE__ */ jsx4("tbody", { children: shown.map((group, i) => /* @__PURE__ */ jsxs4("tr", { children: [
1902
+ editing && group.inputs.map((value, j) => /* @__PURE__ */ jsx4("td", { title: display(value), children: display(value) }, j)),
1903
+ /* @__PURE__ */ jsxs4(
1904
+ "td",
1905
+ {
1906
+ className: group.result.error ? "qt-formula-error" : "",
1907
+ title: group.result.error ?? display(group.result.value),
1908
+ children: [
1909
+ group.result.error ?? display(group.result.value),
1910
+ group.result.regex && /* @__PURE__ */ jsx4(
1911
+ "button",
1912
+ {
1913
+ type: "button",
1914
+ className: "qt-link-btn",
1915
+ onClick: () => setInspection(group.result.regex),
1916
+ children: "Inspect match"
1917
+ }
1918
+ )
1919
+ ]
1920
+ }
1921
+ ),
1922
+ /* @__PURE__ */ jsx4("td", { children: group.count.toLocaleString() }),
1923
+ /* @__PURE__ */ jsxs4("td", { children: [
1924
+ (100 * group.count / preview.processed).toFixed(
1925
+ 1
1926
+ ),
1927
+ "%"
1928
+ ] })
1929
+ ] }, i)) })
1930
+ ] }) }),
1931
+ inspection && /* @__PURE__ */ jsxs4("div", { className: "qt-regex-inspector", children: [
1932
+ /* @__PURE__ */ jsx4("h4", { children: "Regex match" }),
1933
+ /* @__PURE__ */ jsxs4("pre", { children: [
1934
+ inspection.input.slice(0, inspection.start),
1935
+ /* @__PURE__ */ jsx4("mark", { children: inspection.input.slice(
1936
+ inspection.start,
1937
+ inspection.end
1938
+ ) }),
1939
+ inspection.input.slice(inspection.end)
1940
+ ] }),
1941
+ /* @__PURE__ */ jsx4("dl", { children: inspection.groups.map((value, i) => /* @__PURE__ */ jsxs4("div", { children: [
1942
+ /* @__PURE__ */ jsxs4("dt", { children: [
1943
+ "Group ",
1944
+ i,
1945
+ i === 0 ? " (whole match)" : ""
1946
+ ] }),
1947
+ /* @__PURE__ */ jsx4("dd", { children: display(value) })
1948
+ ] }, i)) })
1949
+ ] }),
1950
+ !groups.length && /* @__PURE__ */ jsx4("p", { children: "No matching results in this sample." }),
1951
+ groups.length > 50 && /* @__PURE__ */ jsxs4("div", { className: "qt-preview-pages", children: [
1952
+ /* @__PURE__ */ jsx4(
1953
+ "button",
1954
+ {
1955
+ type: "button",
1956
+ className: "qt-btn",
1957
+ disabled: page === 0,
1958
+ onClick: () => setPage((n) => n - 1),
1959
+ children: "Previous"
1960
+ }
1961
+ ),
1962
+ /* @__PURE__ */ jsxs4("span", { children: [
1963
+ "Combinations ",
1964
+ page * 50 + 1,
1965
+ "\u2013",
1966
+ Math.min((page + 1) * 50, groups.length),
1967
+ " of",
1968
+ " ",
1969
+ groups.length
1970
+ ] }),
1971
+ /* @__PURE__ */ jsx4(
1972
+ "button",
1973
+ {
1974
+ type: "button",
1975
+ className: "qt-btn",
1976
+ disabled: (page + 1) * 50 >= groups.length,
1977
+ onClick: () => setPage((n) => n + 1),
1978
+ children: "Next"
1979
+ }
1980
+ )
1981
+ ] })
1982
+ ] })
1983
+ ] })
1984
+ ] }),
1985
+ /* @__PURE__ */ jsxs4("footer", { className: "qt-select-editor-footer", children: [
1986
+ /* @__PURE__ */ jsx4("span", { className: "qt-muted", children: "Computed columns are display-only. Select at least one column." }),
1987
+ /* @__PURE__ */ jsx4("button", { type: "button", className: "qt-btn", onClick: onClose, children: "Cancel layout changes" }),
1988
+ /* @__PURE__ */ jsx4(
1989
+ "button",
1990
+ {
1991
+ type: "button",
1992
+ className: "qt-btn qt-primary",
1993
+ disabled: !columns.length || columns.length > 200,
1994
+ onClick: () => {
1995
+ api.setQuery((q) => ({ ...q, select: columns }));
1996
+ onClose();
1997
+ },
1998
+ children: "Apply columns"
1999
+ }
2000
+ )
2001
+ ] })
2002
+ ]
2003
+ }
2004
+ ) });
2005
+ }
2006
+
1117
2007
  // src/QueryBuilder.tsx
1118
- import { useEffect as useEffect4, useId, useMemo as useMemo3, useRef as useRef2, useState as useState4 } from "react";
2008
+ import { Fragment as Fragment6, useEffect as useEffect5, useId as useId2, useMemo as useMemo4, useRef as useRef3, useState as useState6 } from "react";
1119
2009
  import {
1120
2010
  NULLARY_OPS as NULLARY_OPS2,
1121
2011
  aggOpNeedsField,
@@ -1124,16 +2014,19 @@ import {
1124
2014
  filterValues as filterValuesFor,
1125
2015
  isGroupable,
1126
2016
  isMeasurable,
2017
+ isOrGroup,
2018
+ opPairsForField as opPairsForField2,
2019
+ predicatesOf,
1127
2020
  queriesEqual,
1128
- isFilterable,
1129
- isSelectable,
2021
+ isFilterable as isFilterable2,
2022
+ isSelectable as isSelectable2,
1130
2023
  isSortable as isSortable2,
1131
2024
  opsForField as opsForField2
1132
2025
  } from "@pythia-software/query-table-core";
1133
2026
 
1134
2027
  // src/FieldPicker.tsx
1135
- import { useMemo as useMemo2, useState as useState3 } from "react";
1136
- import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2028
+ import { useMemo as useMemo3, useState as useState5 } from "react";
2029
+ import { Fragment as Fragment5, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1137
2030
  var UNGROUPED = "other";
1138
2031
  function FieldPicker({
1139
2032
  fields,
@@ -1143,10 +2036,10 @@ function FieldPicker({
1143
2036
  onPick,
1144
2037
  onClose
1145
2038
  }) {
1146
- const [search, setSearch] = useState3("");
2039
+ const [search, setSearch] = useState5("");
1147
2040
  const needle = search.trim().toLowerCase();
1148
- const excludedSet = useMemo2(() => new Set(excluded ?? []), [excluded]);
1149
- const matched = useMemo2(
2041
+ const excludedSet = useMemo3(() => new Set(excluded ?? []), [excluded]);
2042
+ const matched = useMemo3(
1150
2043
  () => fields.filter((f) => {
1151
2044
  if (excludedSet.has(f.name)) return false;
1152
2045
  if (!needle) return true;
@@ -1154,10 +2047,10 @@ function FieldPicker({
1154
2047
  }),
1155
2048
  [fields, excludedSet, needle]
1156
2049
  );
1157
- const grouped = useMemo2(() => groupByGroup(matched), [matched]);
2050
+ const grouped = useMemo3(() => groupByGroup(matched), [matched]);
1158
2051
  const firstEnabled = matched.find((f) => isEnabled(f, stats, gateOnDistinct));
1159
- return /* @__PURE__ */ jsxs3("div", { className: "qt-picker", children: [
1160
- /* @__PURE__ */ jsx3(
2052
+ return /* @__PURE__ */ jsxs5("div", { className: "qt-picker", children: [
2053
+ /* @__PURE__ */ jsx5(
1161
2054
  "input",
1162
2055
  {
1163
2056
  autoFocus: true,
@@ -1176,13 +2069,13 @@ function FieldPicker({
1176
2069
  onBlur: () => setTimeout(() => onClose?.(), 150)
1177
2070
  }
1178
2071
  ),
1179
- /* @__PURE__ */ jsx3("div", { className: "qt-picker-list", children: matched.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "qt-picker-empty", children: "no matches" }) : grouped.map(([group, fs]) => /* @__PURE__ */ jsxs3("div", { className: "qt-picker-group", children: [
1180
- /* @__PURE__ */ jsx3("div", { className: "qt-picker-group-label", children: group }),
2072
+ /* @__PURE__ */ jsx5("div", { className: "qt-picker-list", children: matched.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "qt-picker-empty", children: "no matches" }) : grouped.map(([group, fs]) => /* @__PURE__ */ jsxs5("div", { className: "qt-picker-group", children: [
2073
+ /* @__PURE__ */ jsx5("div", { className: "qt-picker-group-label", children: group }),
1181
2074
  fs.map((f) => {
1182
2075
  const enabled = isEnabled(f, stats, gateOnDistinct);
1183
2076
  const s = stats?.[f.name];
1184
2077
  const aliasHit = aliasMatch(f, needle);
1185
- return /* @__PURE__ */ jsxs3(
2078
+ return /* @__PURE__ */ jsxs5(
1186
2079
  "button",
1187
2080
  {
1188
2081
  type: "button",
@@ -1196,20 +2089,20 @@ function FieldPicker({
1196
2089
  setSearch("");
1197
2090
  },
1198
2091
  children: [
1199
- /* @__PURE__ */ jsxs3("span", { className: "qt-picker-row", children: [
1200
- /* @__PURE__ */ jsx3("span", { className: "qt-picker-label", children: f.label }),
1201
- /* @__PURE__ */ jsx3("span", { className: "qt-picker-name", children: f.name })
2092
+ /* @__PURE__ */ jsxs5("span", { className: "qt-picker-row", children: [
2093
+ /* @__PURE__ */ jsx5("span", { className: "qt-picker-label", children: f.label }),
2094
+ /* @__PURE__ */ jsx5("span", { className: "qt-picker-name", children: f.name })
1202
2095
  ] }),
1203
- aliasHit && /* @__PURE__ */ jsxs3("span", { className: "qt-picker-alias", children: [
2096
+ aliasHit && /* @__PURE__ */ jsxs5("span", { className: "qt-picker-alias", children: [
1204
2097
  "matches: ",
1205
2098
  aliasHit
1206
2099
  ] }),
1207
- s && /* @__PURE__ */ jsxs3("span", { className: "qt-picker-stats", children: [
1208
- s.distinct != null && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2100
+ s && /* @__PURE__ */ jsxs5("span", { className: "qt-picker-stats", children: [
2101
+ s.distinct != null && /* @__PURE__ */ jsxs5(Fragment5, { children: [
1209
2102
  s.distinct,
1210
2103
  " distinct"
1211
2104
  ] }),
1212
- s.min != null && s.max != null && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2105
+ s.min != null && s.max != null && /* @__PURE__ */ jsxs5(Fragment5, { children: [
1213
2106
  " ",
1214
2107
  "\xB7 ",
1215
2108
  fmtStat(s.min),
@@ -1263,17 +2156,17 @@ function fmtStat(v) {
1263
2156
  }
1264
2157
 
1265
2158
  // src/SavedQueriesModal.tsx
1266
- import { useEffect as useEffect3 } from "react";
1267
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2159
+ import { useEffect as useEffect4 } from "react";
2160
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1268
2161
  function SavedQueriesModal({ saved, onClose, onLoad }) {
1269
- useEffect3(() => {
2162
+ useEffect4(() => {
1270
2163
  function onKey(e) {
1271
2164
  if (e.key === "Escape") onClose();
1272
2165
  }
1273
2166
  window.addEventListener("keydown", onKey);
1274
2167
  return () => window.removeEventListener("keydown", onKey);
1275
2168
  }, [onClose]);
1276
- return /* @__PURE__ */ jsx4("div", { className: "qt-modal-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs4(
2169
+ return /* @__PURE__ */ jsx6("div", { className: "qt-modal-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs6(
1277
2170
  "div",
1278
2171
  {
1279
2172
  className: "qt-modal",
@@ -1282,12 +2175,12 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1282
2175
  "aria-modal": "true",
1283
2176
  "aria-label": "Saved queries",
1284
2177
  children: [
1285
- /* @__PURE__ */ jsx4("h3", { className: "qt-modal-title", children: "Saved queries" }),
1286
- saved.loading ? /* @__PURE__ */ jsx4("p", { className: "qt-muted", children: "Loading\u2026" }) : saved.items.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "qt-muted", children: "None saved yet. Save the current query from the toolbar." }) : /* @__PURE__ */ jsx4("ul", { className: "qt-saved-list", children: saved.items.map((s) => {
2178
+ /* @__PURE__ */ jsx6("h3", { className: "qt-modal-title", children: "Saved queries" }),
2179
+ saved.loading ? /* @__PURE__ */ jsx6("p", { className: "qt-muted", children: "Loading\u2026" }) : saved.items.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "qt-muted", children: "None saved yet. Save the current query from the toolbar." }) : /* @__PURE__ */ jsx6("ul", { className: "qt-saved-list", children: saved.items.map((s) => {
1287
2180
  const filters = s.query.where.length;
1288
2181
  const isDefault = saved.defaultId === s.id;
1289
- return /* @__PURE__ */ jsxs4("li", { className: "qt-saved-item", children: [
1290
- /* @__PURE__ */ jsxs4(
2182
+ return /* @__PURE__ */ jsxs6("li", { className: "qt-saved-item", children: [
2183
+ /* @__PURE__ */ jsxs6(
1291
2184
  "button",
1292
2185
  {
1293
2186
  type: "button",
@@ -1298,8 +2191,8 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1298
2191
  onClose();
1299
2192
  },
1300
2193
  children: [
1301
- /* @__PURE__ */ jsx4("span", { className: "qt-saved-name", children: s.name }),
1302
- /* @__PURE__ */ jsxs4("span", { className: "qt-saved-meta qt-muted", children: [
2194
+ /* @__PURE__ */ jsx6("span", { className: "qt-saved-name", children: s.name }),
2195
+ /* @__PURE__ */ jsxs6("span", { className: "qt-saved-meta qt-muted", children: [
1303
2196
  isDefault ? "Default view \xB7 " : "",
1304
2197
  new Date(s.savedAt).toLocaleString(),
1305
2198
  " \xB7 ",
@@ -1310,7 +2203,7 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1310
2203
  ]
1311
2204
  }
1312
2205
  ),
1313
- isDefault ? /* @__PURE__ */ jsx4(
2206
+ isDefault ? /* @__PURE__ */ jsx6(
1314
2207
  "button",
1315
2208
  {
1316
2209
  type: "button",
@@ -1319,7 +2212,7 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1319
2212
  onClick: () => void saved.clearDefault(),
1320
2213
  children: "default"
1321
2214
  }
1322
- ) : /* @__PURE__ */ jsx4(
2215
+ ) : /* @__PURE__ */ jsx6(
1323
2216
  "button",
1324
2217
  {
1325
2218
  type: "button",
@@ -1329,7 +2222,7 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1329
2222
  children: "make default"
1330
2223
  }
1331
2224
  ),
1332
- /* @__PURE__ */ jsx4(
2225
+ /* @__PURE__ */ jsx6(
1333
2226
  "button",
1334
2227
  {
1335
2228
  type: "button",
@@ -1341,14 +2234,14 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1341
2234
  )
1342
2235
  ] }, s.id);
1343
2236
  }) }),
1344
- /* @__PURE__ */ jsx4("div", { className: "qt-modal-footer", children: /* @__PURE__ */ jsx4("button", { type: "button", className: "qt-btn", onClick: onClose, children: "close" }) })
2237
+ /* @__PURE__ */ jsx6("div", { className: "qt-modal-footer", children: /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-btn", onClick: onClose, children: "close" }) })
1345
2238
  ]
1346
2239
  }
1347
2240
  ) });
1348
2241
  }
1349
2242
 
1350
2243
  // src/QueryBuilder.tsx
1351
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2244
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1352
2245
  var cx3 = (...parts) => parts.filter((p) => Boolean(p)).join(" ");
1353
2246
  var MAX_AUTO_REFRESH_POLLS = 1e3;
1354
2247
  var DEFAULT_AUTO_REFRESH_FREQUENCY_MS = 2e4;
@@ -1392,8 +2285,8 @@ function formatStatusTime(ms) {
1392
2285
  return new Date(ms).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", second: "2-digit" });
1393
2286
  }
1394
2287
  function useFieldHasNull(api, fieldName) {
1395
- const [hasNull, setHasNull] = useState4(void 0);
1396
- useEffect4(() => {
2288
+ const [hasNull, setHasNull] = useState6(void 0);
2289
+ useEffect5(() => {
1397
2290
  if (!fieldName) return;
1398
2291
  let cancelled = false;
1399
2292
  void api.filterValues(fieldName, "").then((r) => {
@@ -1409,17 +2302,29 @@ function useFieldHasNull(api, fieldName) {
1409
2302
  }, [api, fieldName]);
1410
2303
  return hasNull;
1411
2304
  }
1412
- function whereClauseAsText(clause, byName) {
2305
+ var CONVERGED_NEGATIVE_OPS = /* @__PURE__ */ new Set(["!=", "is_not_null", "not_matches_regex"]);
2306
+ function positiveOpsForField(field) {
2307
+ return opsForField2(field).filter((op) => !CONVERGED_NEGATIVE_OPS.has(op));
2308
+ }
2309
+ function predicateAsText(clause, byName) {
1413
2310
  const field = byName.get(clause.field)?.label ?? clause.field;
1414
2311
  const op = clause.op.replace(/_/g, " ");
1415
- if (NULLARY_OPS2.has(clause.op)) return `${field} ${op}`;
1416
- const value = clause.value === "" ? "''" : clause.value;
1417
- return `${field} ${op} ${value}`;
2312
+ const not = clause.negated ? "not " : "";
2313
+ if (NULLARY_OPS2.has(clause.op)) return `${field} ${not}${op}`;
2314
+ const strategy = byName.get(clause.field)?.filter?.values;
2315
+ const option = strategy?.source === "static" ? strategy.options.find((o) => typeof o !== "string" && o.value === clause.value) : void 0;
2316
+ const value = clause.value === "" ? "''" : typeof option === "object" ? option.label : clause.value;
2317
+ return `${field} ${not}${op} ${value}`;
2318
+ }
2319
+ function whereTermAsText(term, byName) {
2320
+ if (isOrGroup(term)) return `(${term.any.map((c) => predicateAsText(c, byName)).join(" or ")})`;
2321
+ return predicateAsText(term, byName);
1418
2322
  }
1419
2323
  function orderByAsText(term, byName) {
1420
2324
  const field = byName.get(term.field)?.label ?? term.field;
2325
+ const expression = term.extract ? `regex_extract(${field}, /${term.extract.regex}/)` : field;
1421
2326
  const nulls = term.nulls ? ` nulls ${term.nulls}` : "";
1422
- return `${field} ${term.dir}${nulls}`;
2327
+ return `${expression} ${term.dir}${nulls}`;
1423
2328
  }
1424
2329
  function chipFieldWidthChars(text, fallback = 5, max = 30) {
1425
2330
  return Math.min(max, Math.max(fallback, (text || "").length + 1));
@@ -1428,16 +2333,13 @@ function chipFieldWidthForInput(text, fieldType, fallback = 5, padding = 0) {
1428
2333
  const cap = fieldType === "number" ? 10 : 30;
1429
2334
  return chipFieldWidthChars(text, fallback, cap) + padding;
1430
2335
  }
1431
- function chipFieldWidthForSelect(text, fallback = 3, max = 30) {
1432
- return chipFieldWidthChars(text, fallback, max) + 2;
1433
- }
1434
2336
  function buildCollapsedSummary(where, orderBy, byName) {
1435
- const whereText = where.length === 0 ? "all rows" : where.map((c) => whereClauseAsText(c, byName)).join(" and ");
2337
+ const whereText = whereClauseSummaryText(where, byName);
1436
2338
  const orderText = orderBy.length === 0 ? "(default)" : orderBy.map((term) => orderByAsText(term, byName)).join(", ");
1437
2339
  return `WHERE ${whereText} ORDER BY ${orderText}`;
1438
2340
  }
1439
2341
  function whereClauseSummaryText(where, byName) {
1440
- return where.length === 0 ? "all rows" : where.map((c) => whereClauseAsText(c, byName)).join(" and ");
2342
+ return where.length === 0 ? "all rows" : where.map((t) => whereTermAsText(t, byName)).join(" and ");
1441
2343
  }
1442
2344
  function orderBySummaryText(orderBy, byName) {
1443
2345
  return orderBy.length === 0 ? "(default)" : orderBy.map((term) => orderByAsText(term, byName)).join(", ");
@@ -1453,15 +2355,15 @@ function QueryBuilder({
1453
2355
  defaultCollapsed = false
1454
2356
  }) {
1455
2357
  const UNSAVED_QUERY_EDIT_ID = "__qt-unsaved-query__";
1456
- const [showSaved, setShowSaved] = useState4(false);
1457
- const [shareCopied, setShareCopied] = useState4(false);
1458
- const [showAutoRefresh, setShowAutoRefresh] = useState4(false);
1459
- const [autoRefreshFrequencyMs, setAutoRefreshFrequencyMs] = useState4(DEFAULT_AUTO_REFRESH_FREQUENCY_MS);
1460
- const [autoRefreshTurnOffAfterMs, setAutoRefreshTurnOffAfterMs] = useState4(DEFAULT_AUTO_REFRESH_TURN_OFF_AFTER_MS);
1461
- const [editingSavedId, setEditingSavedId] = useState4(null);
1462
- const [editingSavedName, setEditingSavedName] = useState4("");
1463
- const [lastSavedId, setLastSavedId] = useState4(null);
1464
- const [collapsedFallback, setCollapsedFallback] = useState4(defaultCollapsed);
2358
+ const [showSaved, setShowSaved] = useState6(false);
2359
+ const [shareCopied, setShareCopied] = useState6(false);
2360
+ const [showAutoRefresh, setShowAutoRefresh] = useState6(false);
2361
+ const [autoRefreshFrequencyMs, setAutoRefreshFrequencyMs] = useState6(DEFAULT_AUTO_REFRESH_FREQUENCY_MS);
2362
+ const [autoRefreshTurnOffAfterMs, setAutoRefreshTurnOffAfterMs] = useState6(DEFAULT_AUTO_REFRESH_TURN_OFF_AFTER_MS);
2363
+ const [editingSavedId, setEditingSavedId] = useState6(null);
2364
+ const [editingSavedName, setEditingSavedName] = useState6("");
2365
+ const [lastSavedId, setLastSavedId] = useState6(null);
2366
+ const [collapsedFallback, setCollapsedFallback] = useState6(defaultCollapsed);
1465
2367
  const collapsedControlled = typeof collapsed !== "undefined";
1466
2368
  const isCollapsed = collapsedControlled ? collapsed : collapsedFallback;
1467
2369
  const setCollapsed = (next) => {
@@ -1471,31 +2373,31 @@ function QueryBuilder({
1471
2373
  }
1472
2374
  setCollapsedFallback(next);
1473
2375
  };
1474
- const [lastUpdatedAt, setLastUpdatedAt] = useState4(null);
1475
- const [lastFailureAt, setLastFailureAt] = useState4(null);
1476
- const byName = useMemo3(() => new Map(fields.map((f) => [f.name, f])), [fields]);
1477
- const activeSavedQuery = useMemo3(
2376
+ const [lastUpdatedAt, setLastUpdatedAt] = useState6(null);
2377
+ const [lastFailureAt, setLastFailureAt] = useState6(null);
2378
+ const byName = useMemo4(() => new Map(fields.map((f) => [f.name, f])), [fields]);
2379
+ const activeSavedQuery = useMemo4(
1478
2380
  () => api.saved.items.find((item) => queriesEqual(item.query, api.query)),
1479
2381
  [api.query, api.saved.items]
1480
2382
  );
1481
- const lastSavedQuery = useMemo3(
2383
+ const lastSavedQuery = useMemo4(
1482
2384
  () => api.saved.items.find((item) => item.id === lastSavedId) ?? null,
1483
2385
  [api.saved.items, lastSavedId]
1484
2386
  );
1485
2387
  const isSavedQuery = activeSavedQuery != null;
1486
2388
  const isEditingUnsaved = editingSavedId === UNSAVED_QUERY_EDIT_ID;
1487
- const bodyId = useId();
2389
+ const bodyId = useId2();
1488
2390
  const isEditingSaved = editingSavedId != null && activeSavedQuery?.id === editingSavedId;
1489
2391
  const showUpdateButton = Boolean(lastSavedQuery) && !activeSavedQuery;
1490
- const collapsedSummary = useMemo3(
2392
+ const collapsedSummary = useMemo4(
1491
2393
  () => buildCollapsedSummary(api.query.where, api.query.orderBy, byName),
1492
2394
  [api.query.where, api.query.orderBy, byName]
1493
2395
  );
1494
- const collapsedWhereText = useMemo3(
2396
+ const collapsedWhereText = useMemo4(
1495
2397
  () => whereClauseSummaryText(api.query.where, byName),
1496
2398
  [api.query.where, byName]
1497
2399
  );
1498
- const collapsedOrderText = useMemo3(
2400
+ const collapsedOrderText = useMemo4(
1499
2401
  () => orderBySummaryText(api.query.orderBy, byName),
1500
2402
  [api.query.orderBy, byName]
1501
2403
  );
@@ -1507,27 +2409,27 @@ function QueryBuilder({
1507
2409
  const lastUpdatedText = formatStatusTime(lastUpdatedAt);
1508
2410
  const failureText = api.error ? `Error: ${api.error.message}` : lastFailureAt ? `Last failure at ${formatStatusTime(lastFailureAt)}` : null;
1509
2411
  const isBusy = Boolean(running) || api.loading;
1510
- const lastRowsRef = useRef2(null);
1511
- const lastTotalRef = useRef2(null);
1512
- const previousLoadingRef = useRef2(api.loading);
1513
- const autoRefreshRef = useRef2(null);
1514
- const autoRefreshPopoverRef = useRef2(null);
1515
- const [autoRefreshPopoverAlignRight, setAutoRefreshPopoverAlignRight] = useState4(false);
1516
- const shareFeedbackTimerRef = useRef2(null);
1517
- useEffect4(
2412
+ const lastRowsRef = useRef3(null);
2413
+ const lastTotalRef = useRef3(null);
2414
+ const previousLoadingRef = useRef3(api.loading);
2415
+ const autoRefreshRef = useRef3(null);
2416
+ const autoRefreshPopoverRef = useRef3(null);
2417
+ const [autoRefreshPopoverAlignRight, setAutoRefreshPopoverAlignRight] = useState6(false);
2418
+ const shareFeedbackTimerRef = useRef3(null);
2419
+ useEffect5(
1518
2420
  () => () => {
1519
2421
  if (shareFeedbackTimerRef.current) clearTimeout(shareFeedbackTimerRef.current);
1520
2422
  },
1521
2423
  []
1522
2424
  );
1523
- useEffect4(() => {
2425
+ useEffect5(() => {
1524
2426
  setShareCopied(false);
1525
2427
  if (shareFeedbackTimerRef.current) {
1526
2428
  clearTimeout(shareFeedbackTimerRef.current);
1527
2429
  shareFeedbackTimerRef.current = null;
1528
2430
  }
1529
2431
  }, [api.query]);
1530
- useEffect4(() => {
2432
+ useEffect5(() => {
1531
2433
  if (activeSavedQuery) {
1532
2434
  setLastSavedId(activeSavedQuery.id);
1533
2435
  if (!isEditingSaved) {
@@ -1541,11 +2443,11 @@ function QueryBuilder({
1541
2443
  setEditingSavedName("");
1542
2444
  }
1543
2445
  }, [activeSavedQuery, isEditingSaved, isEditingUnsaved]);
1544
- useEffect4(() => {
2446
+ useEffect5(() => {
1545
2447
  if (!lastSavedId || api.saved.items.some((item) => item.id === lastSavedId)) return;
1546
2448
  setLastSavedId(null);
1547
2449
  }, [api.saved.items, lastSavedId]);
1548
- useEffect4(() => {
2450
+ useEffect5(() => {
1549
2451
  const hasData = api.rows.length > 0 || total != null;
1550
2452
  const rowsChanged = lastRowsRef.current !== api.rows;
1551
2453
  const totalChanged = lastTotalRef.current !== total;
@@ -1640,15 +2542,15 @@ function QueryBuilder({
1640
2542
  return;
1641
2543
  }
1642
2544
  }
1643
- useEffect4(() => {
2545
+ useEffect5(() => {
1644
2546
  if (isCollapsed) setShowSaved(false);
1645
2547
  }, [isCollapsed]);
1646
- useEffect4(() => {
2548
+ useEffect5(() => {
1647
2549
  if (!showAutoRefresh || !autoRefreshStatus) return;
1648
2550
  setAutoRefreshFrequencyMs(autoRefreshStatus.frequencyMs);
1649
2551
  setAutoRefreshTurnOffAfterMs(autoRefreshStatus.turnOffAfterMs);
1650
2552
  }, [showAutoRefresh, autoRefreshStatus]);
1651
- useEffect4(() => {
2553
+ useEffect5(() => {
1652
2554
  if (!showAutoRefresh) return;
1653
2555
  function onDocumentClick(event) {
1654
2556
  const wrapper = autoRefreshRef.current;
@@ -1707,11 +2609,11 @@ function QueryBuilder({
1707
2609
  window.prompt("Copy this query URL", url.toString());
1708
2610
  }
1709
2611
  }
1710
- return /* @__PURE__ */ jsxs5("div", { className: cx3("qt-qb", classNames?.root), children: [
1711
- /* @__PURE__ */ jsxs5("div", { className: "qt-qb-bar qt-qb-state-bar qt-qb-row", children: [
1712
- /* @__PURE__ */ jsxs5("span", { className: "qt-qb-state-main", children: [
1713
- /* @__PURE__ */ jsxs5("span", { className: "qt-qb-saved", children: [
1714
- /* @__PURE__ */ jsx5(
2612
+ return /* @__PURE__ */ jsxs7("div", { className: cx3("qt-qb", classNames?.root), children: [
2613
+ /* @__PURE__ */ jsxs7("div", { className: "qt-qb-bar qt-qb-state-bar qt-qb-row", children: [
2614
+ /* @__PURE__ */ jsxs7("span", { className: "qt-qb-state-main", children: [
2615
+ /* @__PURE__ */ jsxs7("span", { className: "qt-qb-saved", children: [
2616
+ /* @__PURE__ */ jsx7(
1715
2617
  "button",
1716
2618
  {
1717
2619
  type: "button",
@@ -1722,8 +2624,8 @@ function QueryBuilder({
1722
2624
  children: isSavedQuery ? "\u2605" : "\u2606"
1723
2625
  }
1724
2626
  ),
1725
- isEditingSaved || isEditingUnsaved ? /* @__PURE__ */ jsxs5("span", { className: "qt-qb-saved-input-wrap", children: [
1726
- /* @__PURE__ */ jsx5(
2627
+ isEditingSaved || isEditingUnsaved ? /* @__PURE__ */ jsxs7("span", { className: "qt-qb-saved-input-wrap", children: [
2628
+ /* @__PURE__ */ jsx7(
1727
2629
  "input",
1728
2630
  {
1729
2631
  type: "text",
@@ -1746,7 +2648,7 @@ function QueryBuilder({
1746
2648
  onClick: (e) => e.stopPropagation()
1747
2649
  }
1748
2650
  ),
1749
- /* @__PURE__ */ jsx5(
2651
+ /* @__PURE__ */ jsx7(
1750
2652
  "button",
1751
2653
  {
1752
2654
  type: "button",
@@ -1760,7 +2662,7 @@ function QueryBuilder({
1760
2662
  children: "\u2715"
1761
2663
  }
1762
2664
  )
1763
- ] }) : /* @__PURE__ */ jsx5(
2665
+ ] }) : /* @__PURE__ */ jsx7(
1764
2666
  "button",
1765
2667
  {
1766
2668
  type: "button",
@@ -1770,17 +2672,17 @@ function QueryBuilder({
1770
2672
  }
1771
2673
  )
1772
2674
  ] }),
1773
- isCollapsed ? /* @__PURE__ */ jsxs5("span", { className: "qt-qb-summary qt-truncate", title: collapsedSummary, children: [
1774
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-summary-kw", children: "WHERE" }),
2675
+ isCollapsed ? /* @__PURE__ */ jsxs7("span", { className: "qt-qb-summary qt-truncate", title: collapsedSummary, children: [
2676
+ /* @__PURE__ */ jsx7("span", { className: "qt-qb-summary-kw", children: "WHERE" }),
1775
2677
  " ",
1776
- /* @__PURE__ */ jsx5("strong", { children: collapsedWhereText }),
2678
+ /* @__PURE__ */ jsx7("strong", { children: collapsedWhereText }),
1777
2679
  " ",
1778
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-summary-kw", children: "ORDER BY" }),
2680
+ /* @__PURE__ */ jsx7("span", { className: "qt-qb-summary-kw", children: "ORDER BY" }),
1779
2681
  " ",
1780
- /* @__PURE__ */ jsx5("strong", { children: collapsedOrderText })
2682
+ /* @__PURE__ */ jsx7("strong", { children: collapsedOrderText })
1781
2683
  ] }) : null
1782
2684
  ] }),
1783
- showUpdateButton ? /* @__PURE__ */ jsx5(
2685
+ showUpdateButton ? /* @__PURE__ */ jsx7(
1784
2686
  "button",
1785
2687
  {
1786
2688
  type: "button",
@@ -1790,7 +2692,7 @@ function QueryBuilder({
1790
2692
  children: `Update Query '${lastSavedQuery?.name ?? "query"}'`
1791
2693
  }
1792
2694
  ) : null,
1793
- /* @__PURE__ */ jsx5(
2695
+ /* @__PURE__ */ jsx7(
1794
2696
  "button",
1795
2697
  {
1796
2698
  type: "button",
@@ -1801,11 +2703,11 @@ function QueryBuilder({
1801
2703
  children: shareCopied ? "Copied!" : "Share"
1802
2704
  }
1803
2705
  ),
1804
- /* @__PURE__ */ jsxs5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: () => setShowSaved(true), children: [
2706
+ /* @__PURE__ */ jsxs7("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: () => setShowSaved(true), children: [
1805
2707
  "Saved",
1806
2708
  api.saved.items.length > 0 ? ` (${api.saved.items.length})` : ""
1807
2709
  ] }),
1808
- /* @__PURE__ */ jsxs5(
2710
+ /* @__PURE__ */ jsxs7(
1809
2711
  "button",
1810
2712
  {
1811
2713
  type: "button",
@@ -1815,7 +2717,7 @@ function QueryBuilder({
1815
2717
  "aria-controls": bodyId,
1816
2718
  title: isCollapsed ? "Expand query builder" : "Collapse query builder",
1817
2719
  children: [
1818
- /* @__PURE__ */ jsx5(
2720
+ /* @__PURE__ */ jsx7(
1819
2721
  "span",
1820
2722
  {
1821
2723
  className: cx3("qt-qb-collapse-caret", isCollapsed && "qt-qb-collapse-caret--collapsed"),
@@ -1823,38 +2725,38 @@ function QueryBuilder({
1823
2725
  children: "\u25BE"
1824
2726
  }
1825
2727
  ),
1826
- /* @__PURE__ */ jsx5("span", { className: "qt-sr-only", children: isCollapsed ? "Expand query builder" : "Collapse query builder" })
2728
+ /* @__PURE__ */ jsx7("span", { className: "qt-sr-only", children: isCollapsed ? "Expand query builder" : "Collapse query builder" })
1827
2729
  ]
1828
2730
  }
1829
2731
  )
1830
2732
  ] }),
1831
- !isCollapsed ? /* @__PURE__ */ jsxs5("div", { className: "qt-qb-bar qt-qb-editor-bar", children: [
1832
- /* @__PURE__ */ jsxs5("div", { id: bodyId, className: "qt-qb-body", children: [
1833
- /* @__PURE__ */ jsx5(SelectRow, { api, fields, classNames, disabled: isBusy }),
1834
- /* @__PURE__ */ jsx5(WhereRow, { api, byName, fields, classNames, disabled: isBusy }),
1835
- /* @__PURE__ */ jsx5(OrderRow, { api, fields, classNames, disabled: isBusy }),
1836
- /* @__PURE__ */ jsx5(WindowRow, { api, classNames, disabled: isBusy }),
1837
- /* @__PURE__ */ jsx5(MetricsRow, { api, fields, classNames, disabled: isBusy })
2733
+ !isCollapsed ? /* @__PURE__ */ jsxs7("div", { className: "qt-qb-bar qt-qb-editor-bar", children: [
2734
+ /* @__PURE__ */ jsxs7("div", { id: bodyId, className: "qt-qb-body", children: [
2735
+ /* @__PURE__ */ jsx7(SelectRow, { api, fields, classNames, disabled: isBusy }),
2736
+ /* @__PURE__ */ jsx7(WhereRow, { api, byName, fields, classNames, disabled: isBusy }),
2737
+ /* @__PURE__ */ jsx7(OrderRow, { api, fields, classNames, disabled: isBusy }),
2738
+ /* @__PURE__ */ jsx7(WindowRow, { api, classNames, disabled: isBusy }),
2739
+ /* @__PURE__ */ jsx7(MetricsRow, { api, fields, classNames, disabled: isBusy })
1838
2740
  ] }),
1839
- /* @__PURE__ */ jsxs5("div", { className: "qt-qb-editor-stack", children: [
1840
- /* @__PURE__ */ jsx5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.undo, disabled: !api.canUndo || isBusy, children: "\u21B6 Undo" }),
1841
- /* @__PURE__ */ jsx5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.redo, disabled: !api.canRedo || isBusy, children: "\u21B7 Redo" }),
1842
- /* @__PURE__ */ jsx5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.resetAll, disabled: isBusy, children: "Reset All" })
2741
+ /* @__PURE__ */ jsxs7("div", { className: "qt-qb-editor-stack", children: [
2742
+ /* @__PURE__ */ jsx7("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.undo, disabled: !api.canUndo || isBusy, children: "\u21B6 Undo" }),
2743
+ /* @__PURE__ */ jsx7("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.redo, disabled: !api.canRedo || isBusy, children: "\u21B7 Redo" }),
2744
+ /* @__PURE__ */ jsx7("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.resetAll, disabled: isBusy, children: "Reset All" })
1843
2745
  ] })
1844
2746
  ] }) : null,
1845
- /* @__PURE__ */ jsxs5("div", { className: "qt-qb-bar qt-qb-run-bar qt-qb-row", children: [
1846
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-run-metric qt-qb-run-metric--strong", children: api.loading ? "loading\u2026" : `${rowSummary} rows` }),
1847
- /* @__PURE__ */ jsxs5("span", { className: "qt-qb-run-metric", children: [
2747
+ /* @__PURE__ */ jsxs7("div", { className: "qt-qb-bar qt-qb-run-bar qt-qb-row", children: [
2748
+ /* @__PURE__ */ jsx7("span", { className: "qt-qb-run-metric qt-qb-run-metric--strong", children: api.loading ? "loading\u2026" : `${rowSummary} rows` }),
2749
+ /* @__PURE__ */ jsxs7("span", { className: "qt-qb-run-metric", children: [
1848
2750
  "Last updated ",
1849
2751
  lastUpdatedText
1850
2752
  ] }),
1851
- failureText ? /* @__PURE__ */ jsx5("span", { className: "qt-qb-run-error", children: failureText }) : null,
1852
- /* @__PURE__ */ jsxs5("span", { className: "qt-qb-run-actions", children: [
1853
- autoRefreshStatus ? /* @__PURE__ */ jsx5("span", { className: "qt-qb-auto-status", children: `Every ${formatDuration2(autoRefreshStatus.frequencyMs)}; clear after ${formatDuration2(
2753
+ failureText ? /* @__PURE__ */ jsx7("span", { className: "qt-qb-run-error", children: failureText }) : null,
2754
+ /* @__PURE__ */ jsxs7("span", { className: "qt-qb-run-actions", children: [
2755
+ autoRefreshStatus ? /* @__PURE__ */ jsx7("span", { className: "qt-qb-auto-status", children: `Every ${formatDuration2(autoRefreshStatus.frequencyMs)}; clear after ${formatDuration2(
1854
2756
  autoRefreshStatus.turnOffAfterMs
1855
2757
  )} (${autoRefreshStatus.pollCount} polls run)` }) : null,
1856
- /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh", ref: autoRefreshRef, children: [
1857
- /* @__PURE__ */ jsx5(
2758
+ /* @__PURE__ */ jsxs7("span", { className: "qt-auto-refresh", ref: autoRefreshRef, children: [
2759
+ /* @__PURE__ */ jsx7(
1858
2760
  "button",
1859
2761
  {
1860
2762
  type: "button",
@@ -1865,7 +2767,7 @@ function QueryBuilder({
1865
2767
  children: autoRefreshButtonText
1866
2768
  }
1867
2769
  ),
1868
- showAutoRefresh ? /* @__PURE__ */ jsxs5(
2770
+ showAutoRefresh ? /* @__PURE__ */ jsxs7(
1869
2771
  "span",
1870
2772
  {
1871
2773
  ref: autoRefreshPopoverRef,
@@ -1873,27 +2775,27 @@ function QueryBuilder({
1873
2775
  role: "dialog",
1874
2776
  "aria-label": "Auto-refresh settings",
1875
2777
  children: [
1876
- /* @__PURE__ */ jsxs5("label", { className: "qt-auto-refresh-field", children: [
1877
- /* @__PURE__ */ jsx5("span", { children: "Frequency" }),
1878
- /* @__PURE__ */ jsx5(
2778
+ /* @__PURE__ */ jsxs7("label", { className: "qt-auto-refresh-field", children: [
2779
+ /* @__PURE__ */ jsx7("span", { children: "Frequency" }),
2780
+ /* @__PURE__ */ jsx7(
1879
2781
  "select",
1880
2782
  {
1881
2783
  className: "qt-auto-refresh-select",
1882
2784
  value: autoRefreshFrequencyMs,
1883
2785
  onChange: (e) => setAutoRefreshFrequencyMs(Number(e.target.value)),
1884
- children: AUTO_REFRESH_FREQUENCIES.map((option) => /* @__PURE__ */ jsx5("option", { value: option.value, children: option.label }, option.value))
2786
+ children: AUTO_REFRESH_FREQUENCIES.map((option) => /* @__PURE__ */ jsx7("option", { value: option.value, children: option.label }, option.value))
1885
2787
  }
1886
2788
  )
1887
2789
  ] }),
1888
- /* @__PURE__ */ jsxs5("label", { className: "qt-auto-refresh-field", children: [
1889
- /* @__PURE__ */ jsx5("span", { children: "Turn off after" }),
1890
- /* @__PURE__ */ jsx5(
2790
+ /* @__PURE__ */ jsxs7("label", { className: "qt-auto-refresh-field", children: [
2791
+ /* @__PURE__ */ jsx7("span", { children: "Turn off after" }),
2792
+ /* @__PURE__ */ jsx7(
1891
2793
  "select",
1892
2794
  {
1893
2795
  className: "qt-auto-refresh-select",
1894
2796
  value: autoRefreshTurnOffAfterMs,
1895
2797
  onChange: (e) => setAutoRefreshTurnOffAfterMs(Number(e.target.value)),
1896
- children: AUTO_REFRESH_TURN_OFF_AFTER.map((option) => /* @__PURE__ */ jsx5(
2798
+ children: AUTO_REFRESH_TURN_OFF_AFTER.map((option) => /* @__PURE__ */ jsx7(
1897
2799
  "option",
1898
2800
  {
1899
2801
  value: option.value,
@@ -1905,8 +2807,8 @@ function QueryBuilder({
1905
2807
  }
1906
2808
  )
1907
2809
  ] }),
1908
- /* @__PURE__ */ jsx5("span", { className: cx3("qt-auto-refresh-status", !canSubmitAutoRefresh && "qt-auto-refresh-status--error"), children: canSubmitAutoRefresh ? `${selectedAutoRefreshPolls} polls maximum` : `Choose between 1 and 1000 polls` }),
1909
- autoRefreshStatus ? /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh-status", children: [
2810
+ /* @__PURE__ */ jsx7("span", { className: cx3("qt-auto-refresh-status", !canSubmitAutoRefresh && "qt-auto-refresh-status--error"), children: canSubmitAutoRefresh ? `${selectedAutoRefreshPolls} polls maximum` : `Choose between 1 and 1000 polls` }),
2811
+ autoRefreshStatus ? /* @__PURE__ */ jsxs7("span", { className: "qt-auto-refresh-status", children: [
1910
2812
  "Active: every ",
1911
2813
  formatDuration2(autoRefreshStatus.frequencyMs),
1912
2814
  ", clears after",
@@ -1916,8 +2818,8 @@ function QueryBuilder({
1916
2818
  autoRefreshStatus.pollCount,
1917
2819
  " polls run."
1918
2820
  ] }) : null,
1919
- /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh-actions", children: [
1920
- /* @__PURE__ */ jsx5(
2821
+ /* @__PURE__ */ jsxs7("span", { className: "qt-auto-refresh-actions", children: [
2822
+ /* @__PURE__ */ jsx7(
1921
2823
  "button",
1922
2824
  {
1923
2825
  type: "button",
@@ -1927,7 +2829,7 @@ function QueryBuilder({
1927
2829
  children: autoRefreshStatus ? "Update" : "Start"
1928
2830
  }
1929
2831
  ),
1930
- autoRefreshStatus ? /* @__PURE__ */ jsx5(
2832
+ autoRefreshStatus ? /* @__PURE__ */ jsx7(
1931
2833
  "button",
1932
2834
  {
1933
2835
  type: "button",
@@ -1944,7 +2846,7 @@ function QueryBuilder({
1944
2846
  }
1945
2847
  ) : null
1946
2848
  ] }),
1947
- /* @__PURE__ */ jsx5(
2849
+ /* @__PURE__ */ jsx7(
1948
2850
  "button",
1949
2851
  {
1950
2852
  type: "button",
@@ -1956,7 +2858,7 @@ function QueryBuilder({
1956
2858
  )
1957
2859
  ] })
1958
2860
  ] }),
1959
- !isCollapsed && showSaved ? /* @__PURE__ */ jsx5(SavedQueriesModal, { saved: api.saved, onClose: () => setShowSaved(false), onLoad: setLastSavedId }) : null
2861
+ !isCollapsed && showSaved ? /* @__PURE__ */ jsx7(SavedQueriesModal, { saved: api.saved, onClose: () => setShowSaved(false), onLoad: setLastSavedId }) : null
1960
2862
  ] });
1961
2863
  }
1962
2864
  function MoveButtons({
@@ -1967,8 +2869,8 @@ function MoveButtons({
1967
2869
  onMove
1968
2870
  }) {
1969
2871
  if (count < 2) return null;
1970
- return /* @__PURE__ */ jsxs5("span", { className: "qt-chip-move-buttons", children: [
1971
- /* @__PURE__ */ jsx5(
2872
+ return /* @__PURE__ */ jsxs7("span", { className: "qt-chip-move-buttons", children: [
2873
+ /* @__PURE__ */ jsx7(
1972
2874
  "button",
1973
2875
  {
1974
2876
  type: "button",
@@ -1980,7 +2882,7 @@ function MoveButtons({
1980
2882
  children: "\u2190"
1981
2883
  }
1982
2884
  ),
1983
- /* @__PURE__ */ jsx5(
2885
+ /* @__PURE__ */ jsx7(
1984
2886
  "button",
1985
2887
  {
1986
2888
  type: "button",
@@ -2001,19 +2903,22 @@ function SelectRow({
2001
2903
  disabled
2002
2904
  }) {
2003
2905
  const { select, columnDrag } = api;
2004
- const [adding, setAdding] = useState4(false);
2005
- const fieldLabelByName = useMemo3(() => new Map(select.fields.map((f) => [f.name, f.label])), [select.fields]);
2906
+ const [editingColumns, setEditingColumns] = useState6(false);
2907
+ const [adding, setAdding] = useState6(false);
2908
+ const fieldLabelByName = useMemo4(() => new Map(select.fields.map((f) => [f.name, f.label])), [select.fields]);
2006
2909
  function reorderByIndex(from, overIndex) {
2007
2910
  select.move(from, overIndex);
2008
2911
  }
2009
2912
  const fieldNames = select.visible.map((c) => c.field);
2010
2913
  const dragSource = columnDrag.source;
2011
2914
  const rendered = columnDrag.preview(fieldNames);
2012
- return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row", children: [
2013
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "select" }),
2915
+ return /* @__PURE__ */ jsxs7("div", { className: "qt-qb-row", children: [
2916
+ /* @__PURE__ */ jsx7("span", { className: "qt-qb-kw", children: "select" }),
2917
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-btn", disabled, onClick: () => setEditingColumns(true), children: "Edit columns" }),
2918
+ editingColumns && /* @__PURE__ */ jsx7(SelectColumnEditor, { api, onClose: () => setEditingColumns(false) }),
2014
2919
  rendered.map((name, position) => {
2015
2920
  const label = fieldLabelByName.get(name) ?? name;
2016
- return /* @__PURE__ */ jsxs5(
2921
+ return /* @__PURE__ */ jsxs7(
2017
2922
  "span",
2018
2923
  {
2019
2924
  className: cx3(
@@ -2052,9 +2957,9 @@ function SelectRow({
2052
2957
  onDragEnd: () => columnDrag.end(),
2053
2958
  title: "drag to reorder",
2054
2959
  children: [
2055
- /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2056
- /* @__PURE__ */ jsx5("span", { className: "qt-chip-field", children: label }),
2057
- /* @__PURE__ */ jsx5(
2960
+ /* @__PURE__ */ jsx7("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2961
+ /* @__PURE__ */ jsx7("span", { className: "qt-chip-field", children: label }),
2962
+ /* @__PURE__ */ jsx7(
2058
2963
  MoveButtons,
2059
2964
  {
2060
2965
  label,
@@ -2064,23 +2969,23 @@ function SelectRow({
2064
2969
  onMove: (nextPosition) => select.move(name, nextPosition)
2065
2970
  }
2066
2971
  ),
2067
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => select.hide(name), disabled, children: "\u2715" })
2972
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-chip-x", onClick: () => select.hide(name), disabled, children: "\u2715" })
2068
2973
  ]
2069
2974
  },
2070
2975
  name
2071
2976
  );
2072
2977
  }),
2073
- adding ? /* @__PURE__ */ jsx5(
2978
+ adding ? /* @__PURE__ */ jsx7(
2074
2979
  FieldPicker,
2075
2980
  {
2076
- fields: select.hidden.filter(isSelectable),
2981
+ fields: select.hidden.filter(isSelectable2),
2077
2982
  onPick: (f) => {
2078
2983
  select.show(f.name);
2079
2984
  setAdding(false);
2080
2985
  },
2081
2986
  onClose: () => setAdding(false)
2082
2987
  }
2083
- ) : /* @__PURE__ */ jsx5(
2988
+ ) : /* @__PURE__ */ jsx7(
2084
2989
  "button",
2085
2990
  {
2086
2991
  type: "button",
@@ -2090,9 +2995,16 @@ function SelectRow({
2090
2995
  children: "+ add column"
2091
2996
  }
2092
2997
  ),
2093
- api.query.select.length > 0 && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: select.reset, disabled, title: "Reset to default columns", children: "reset" })
2998
+ api.query.select.length > 0 && /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-link-btn", onClick: select.reset, disabled, title: "Reset to default columns", children: "reset" })
2094
2999
  ] });
2095
3000
  }
3001
+ function dropModeFor(e) {
3002
+ const rect = e.currentTarget.getBoundingClientRect();
3003
+ const x = e.clientX - rect.left;
3004
+ if (x < rect.width * 0.3) return "before";
3005
+ if (x > rect.width * 0.7) return "after";
3006
+ return "merge";
3007
+ }
2096
3008
  function WhereRow({
2097
3009
  api,
2098
3010
  byName,
@@ -2100,148 +3012,319 @@ function WhereRow({
2100
3012
  classNames,
2101
3013
  disabled
2102
3014
  }) {
2103
- const [adding, setAdding] = useState4(false);
3015
+ const [adding, setAdding] = useState6(false);
3016
+ const [drag, setDrag] = useState6(null);
3017
+ const where = api.query.where;
3018
+ const hasOr = where.some(isOrGroup);
2104
3019
  function addClause(f) {
2105
- const op = opsForField2(f)[0] ?? "=";
3020
+ const op = positiveOpsForField(f)[0] ?? opsForField2(f)[0] ?? "=";
2106
3021
  api.addFilter({ field: f.name, op, value: "" });
2107
3022
  setAdding(false);
2108
3023
  }
2109
- const groupedWhere = useMemo3(() => {
2110
- const groups = /* @__PURE__ */ new Map();
2111
- const order = [];
2112
- api.query.where.forEach((c, i) => {
2113
- if (!groups.has(c.field)) order.push(c.field);
2114
- const group = groups.get(c.field) ?? { field: c.field, clauses: [] };
2115
- group.clauses.push({ clause: c, index: i });
2116
- groups.set(c.field, group);
2117
- });
2118
- return order.map((fieldName) => groups.get(fieldName));
2119
- }, [api.query.where]);
2120
- return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row", children: [
2121
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "where" }),
2122
- api.query.where.length === 0 && !adding && /* @__PURE__ */ jsx5("span", { className: "qt-qb-hint", children: "all rows" }),
2123
- groupedWhere.map((g) => /* @__PURE__ */ jsx5(
2124
- ClauseChip,
2125
- {
2126
- api,
2127
- field: byName.get(g.field),
2128
- clauses: g.clauses,
2129
- classNames,
2130
- disabled,
2131
- onChange: (index, next) => api.updateFilter(index, next),
2132
- onRemove: (index) => api.removeFilter(index)
2133
- },
2134
- g.field
2135
- )),
2136
- adding ? /* @__PURE__ */ jsx5(
3024
+ function handleOver(e, index) {
3025
+ if (!drag) return;
3026
+ e.preventDefault();
3027
+ e.dataTransfer.dropEffect = "move";
3028
+ const mode = dropModeFor(e);
3029
+ setDrag((d) => d && (d.over?.index !== index || d.over?.mode !== mode) ? { from: d.from, over: { index, mode } } : d);
3030
+ }
3031
+ function handleDrop() {
3032
+ if (!drag || !drag.over) {
3033
+ setDrag(null);
3034
+ return;
3035
+ }
3036
+ const { from } = drag;
3037
+ const { index: target, mode } = drag.over;
3038
+ if (mode === "merge") {
3039
+ if (target !== from) api.mergeFilters(from, target);
3040
+ } else {
3041
+ const tPrime = target < from ? target : target - 1;
3042
+ const insertAt = mode === "after" ? tPrime + 1 : tPrime;
3043
+ if (insertAt !== from) api.reorderFilters(from, insertAt);
3044
+ }
3045
+ setDrag(null);
3046
+ }
3047
+ return /* @__PURE__ */ jsxs7("div", { className: "qt-qb-row", children: [
3048
+ /* @__PURE__ */ jsx7("span", { className: "qt-qb-kw", children: "where" }),
3049
+ where.length === 0 && !adding && /* @__PURE__ */ jsx7("span", { className: "qt-qb-hint", children: "all rows" }),
3050
+ where.map((term, index) => /* @__PURE__ */ jsxs7("span", { className: "qt-where-term-wrap", children: [
3051
+ index > 0 && hasOr && /* @__PURE__ */ jsx7("span", { className: "qt-where-and", children: "AND" }),
3052
+ /* @__PURE__ */ jsx7(
3053
+ TermChip,
3054
+ {
3055
+ api,
3056
+ term,
3057
+ index,
3058
+ byName,
3059
+ classNames,
3060
+ disabled,
3061
+ isDragSource: drag?.from === index,
3062
+ dropHint: drag?.over?.index === index ? drag.over.mode : null,
3063
+ onDragStart: () => setDrag({ from: index, over: null }),
3064
+ onDragOver: (e) => handleOver(e, index),
3065
+ onDrop: handleDrop,
3066
+ onDragEnd: () => setDrag(null)
3067
+ }
3068
+ )
3069
+ ] }, index)),
3070
+ adding ? /* @__PURE__ */ jsx7(
2137
3071
  FieldPicker,
2138
3072
  {
2139
- fields: fields.filter(isFilterable),
3073
+ fields: fields.filter(isFilterable2),
2140
3074
  gateOnDistinct: true,
2141
3075
  onPick: addClause,
2142
3076
  onClose: () => setAdding(false)
2143
3077
  }
2144
- ) : /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add filter" }),
2145
- api.query.where.length > 0 && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: api.clearFilters, disabled, title: "Reset filters", children: "reset" })
3078
+ ) : /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add filter" }),
3079
+ where.length > 0 && /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-link-btn", onClick: api.clearFilters, disabled, title: "Reset filters", children: "reset" })
2146
3080
  ] });
2147
3081
  }
2148
- function ClauseChip({
3082
+ function TermChip({
2149
3083
  api,
2150
- field,
2151
- clauses,
3084
+ term,
3085
+ index,
3086
+ byName,
2152
3087
  classNames,
2153
3088
  disabled,
2154
- onChange,
2155
- onRemove
3089
+ isDragSource,
3090
+ dropHint,
3091
+ onDragStart,
3092
+ onDragOver,
3093
+ onDrop,
3094
+ onDragEnd
2156
3095
  }) {
2157
- const labelField = field?.label;
2158
- const fieldName = clauses[0]?.clause.field;
2159
- const fieldHasNull = useFieldHasNull(api, field?.name);
2160
- return /* @__PURE__ */ jsxs5("span", { className: cx3("qt-chip", "qt-chip--where", classNames?.chip), children: [
2161
- /* @__PURE__ */ jsx5("span", { className: "qt-chip-field", children: labelField ?? fieldName }),
2162
- /* @__PURE__ */ jsx5("span", { className: "qt-chip-where-list", children: clauses.map((entry, idx) => {
2163
- const clause = entry.clause;
2164
- const baseOps = field ? opsForField2(field) : [clause.op];
2165
- const filteredBaseOps = fieldHasNull === false ? baseOps.filter((op) => !NULLARY_OPS2.has(op)) : baseOps;
2166
- const clauseOps = filteredBaseOps.includes(clause.op) ? filteredBaseOps : [...filteredBaseOps, clause.op];
2167
- const needsValue = !NULLARY_OPS2.has(clause.op);
2168
- return /* @__PURE__ */ jsxs5("span", { className: "qt-chip-where-clause", children: [
2169
- idx > 0 && /* @__PURE__ */ jsx5("span", { className: "qt-chip-and", children: "and" }),
2170
- /* @__PURE__ */ jsx5(
2171
- "select",
3096
+ const group = isOrGroup(term);
3097
+ const predicates = predicatesOf(term);
3098
+ const grip = /* @__PURE__ */ jsx7(
3099
+ "span",
3100
+ {
3101
+ className: "qt-chip-grip",
3102
+ "aria-hidden": true,
3103
+ draggable: !disabled,
3104
+ onDragStart: (e) => {
3105
+ onDragStart();
3106
+ e.dataTransfer.effectAllowed = "move";
3107
+ e.dataTransfer.setData("text/plain", String(index));
3108
+ },
3109
+ onDragEnd,
3110
+ title: "drag to reorder, or drop onto another filter to make an OR",
3111
+ children: "\u22EE\u22EE"
3112
+ }
3113
+ );
3114
+ return /* @__PURE__ */ jsxs7(
3115
+ "span",
3116
+ {
3117
+ className: cx3(
3118
+ "qt-chip",
3119
+ "qt-chip--where",
3120
+ group && "qt-chip--or",
3121
+ isDragSource && "qt-chip--dragging",
3122
+ dropHint ? `qt-chip--drop-${dropHint}` : void 0,
3123
+ classNames?.chip
3124
+ ),
3125
+ onDragOver,
3126
+ onDrop: (e) => {
3127
+ e.preventDefault();
3128
+ onDrop();
3129
+ },
3130
+ children: [
3131
+ grip,
3132
+ group ? /* @__PURE__ */ jsx7("span", { className: "qt-or-group", children: predicates.map((clause, predIndex) => /* @__PURE__ */ jsxs7("span", { className: "qt-or-member", children: [
3133
+ predIndex > 0 && /* @__PURE__ */ jsx7("span", { className: "qt-chip-and qt-chip-or", children: "OR" }),
3134
+ /* @__PURE__ */ jsx7(
3135
+ PredicateEditor,
3136
+ {
3137
+ api,
3138
+ field: byName.get(clause.field),
3139
+ clause,
3140
+ classNames,
3141
+ disabled,
3142
+ onChange: (next) => api.updatePredicate(index, predIndex, next),
3143
+ onRemove: () => api.removePredicate(index, predIndex)
3144
+ }
3145
+ )
3146
+ ] }, predIndex)) }) : /* @__PURE__ */ jsx7(
3147
+ PredicateEditor,
2172
3148
  {
2173
- className: cx3("qt-chip-op", classNames?.select),
2174
- value: clause.op,
3149
+ api,
3150
+ field: byName.get(term.field),
3151
+ clause: term,
3152
+ classNames,
2175
3153
  disabled,
2176
- style: { width: `${chipFieldWidthForSelect(clause.op, 3)}ch` },
2177
- onChange: (e) => onChange(entry.index, { ...clause, op: e.target.value }),
2178
- children: clauseOps.map((op) => /* @__PURE__ */ jsx5("option", { value: op, children: op }, op))
3154
+ onChange: (next) => api.updatePredicate(index, 0, next),
3155
+ onRemove: () => api.removeFilter(index)
2179
3156
  }
2180
- ),
2181
- needsValue && /* @__PURE__ */ jsx5(
2182
- ValueInput,
3157
+ )
3158
+ ]
3159
+ }
3160
+ );
3161
+ }
3162
+ var OP_LABEL = {
3163
+ "=": "=",
3164
+ "!=": "\u2260",
3165
+ ">": ">",
3166
+ ">=": "\u2265",
3167
+ "<": "<",
3168
+ "<=": "\u2264",
3169
+ contains: "contains",
3170
+ starts_with: "starts with",
3171
+ ends_with: "ends with",
3172
+ matches_regex: "matches",
3173
+ not_matches_regex: "not matches",
3174
+ includes: "includes",
3175
+ is_null: "is null",
3176
+ is_not_null: "is not null"
3177
+ };
3178
+ function choiceLabel(op, negated) {
3179
+ return negated ? `not ${OP_LABEL[op]}` : OP_LABEL[op];
3180
+ }
3181
+ function OpPicker({
3182
+ field,
3183
+ clause,
3184
+ disabled,
3185
+ classNames,
3186
+ onPick
3187
+ }) {
3188
+ const [open, setOpen] = useState6(false);
3189
+ const wrapRef = useRef3(null);
3190
+ useEffect5(() => {
3191
+ if (!open) return;
3192
+ function onDocClick(e) {
3193
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
3194
+ }
3195
+ function onKey(e) {
3196
+ if (e.key === "Escape") setOpen(false);
3197
+ }
3198
+ const t = setTimeout(() => window.addEventListener("click", onDocClick), 0);
3199
+ window.addEventListener("keydown", onKey);
3200
+ return () => {
3201
+ clearTimeout(t);
3202
+ window.removeEventListener("click", onDocClick);
3203
+ window.removeEventListener("keydown", onKey);
3204
+ };
3205
+ }, [open]);
3206
+ const pairs = useMemo4(() => field ? opPairsForField2(field) : [], [field]);
3207
+ const isActive = (choice) => choice.op === clause.op && Boolean(choice.negated) === Boolean(clause.negated);
3208
+ return /* @__PURE__ */ jsxs7("span", { className: "qt-op-picker", ref: wrapRef, children: [
3209
+ /* @__PURE__ */ jsxs7(
3210
+ "button",
3211
+ {
3212
+ type: "button",
3213
+ className: cx3("qt-chip-op-btn", "qt-op-trigger", classNames?.select),
3214
+ disabled,
3215
+ "aria-haspopup": "dialog",
3216
+ "aria-expanded": open,
3217
+ onClick: () => setOpen((o) => !o),
3218
+ children: [
3219
+ choiceLabel(clause.op, Boolean(clause.negated)),
3220
+ /* @__PURE__ */ jsx7("span", { className: "qt-op-caret", "aria-hidden": true, children: "\u25BE" })
3221
+ ]
3222
+ }
3223
+ ),
3224
+ open && pairs.length > 0 && /* @__PURE__ */ jsxs7("span", { className: "qt-op-pop", role: "dialog", "aria-label": "Choose condition", children: [
3225
+ /* @__PURE__ */ jsx7("span", { className: "qt-cm-col-head", children: "keep" }),
3226
+ /* @__PURE__ */ jsx7("span", { className: "qt-cm-col-head qt-cm-col-head--neg", children: "exclude" }),
3227
+ pairs.map((pair, i) => /* @__PURE__ */ jsxs7(Fragment6, { children: [
3228
+ /* @__PURE__ */ jsx7(
3229
+ "button",
2183
3230
  {
2184
- api,
2185
- field,
2186
- value: clause.value,
2187
- classNames,
2188
- onChange: (v) => onChange(entry.index, { ...clause, value: v })
3231
+ type: "button",
3232
+ className: cx3("qt-op-cell", isActive(pair.keep) && "qt-op-cell--on"),
3233
+ onClick: () => {
3234
+ onPick(pair.keep);
3235
+ setOpen(false);
3236
+ },
3237
+ children: choiceLabel(pair.keep.op, pair.keep.negated)
2189
3238
  }
2190
3239
  ),
2191
- /* @__PURE__ */ jsx5(
3240
+ /* @__PURE__ */ jsx7(
2192
3241
  "button",
2193
3242
  {
2194
3243
  type: "button",
2195
- className: "qt-chip-x",
2196
- onClick: () => onRemove(entry.index),
2197
- disabled,
2198
- children: "\u2715"
3244
+ className: cx3("qt-op-cell", "qt-op-cell--neg", isActive(pair.exclude) && "qt-op-cell--on"),
3245
+ onClick: () => {
3246
+ onPick(pair.exclude);
3247
+ setOpen(false);
3248
+ },
3249
+ children: choiceLabel(pair.exclude.op, pair.exclude.negated)
2199
3250
  }
2200
3251
  )
2201
- ] }, entry.index);
2202
- }) })
3252
+ ] }, i))
3253
+ ] })
3254
+ ] });
3255
+ }
3256
+ function PredicateEditor({
3257
+ api,
3258
+ field,
3259
+ clause,
3260
+ classNames,
3261
+ disabled,
3262
+ onChange,
3263
+ onRemove
3264
+ }) {
3265
+ const needsValue = !NULLARY_OPS2.has(clause.op);
3266
+ const isRegex = clause.op === "matches_regex" || clause.op === "not_matches_regex";
3267
+ function pickOp(choice) {
3268
+ const next = { field: clause.field, op: choice.op, value: clause.value };
3269
+ if (choice.negated) next.negated = true;
3270
+ onChange(next);
3271
+ }
3272
+ return /* @__PURE__ */ jsxs7("span", { className: "qt-pred", children: [
3273
+ /* @__PURE__ */ jsx7("span", { className: "qt-chip-field", children: field?.label ?? clause.field }),
3274
+ /* @__PURE__ */ jsx7(OpPicker, { field, clause, disabled, classNames, onPick: pickOp }),
3275
+ needsValue && /* @__PURE__ */ jsx7(
3276
+ ValueInput,
3277
+ {
3278
+ api,
3279
+ field,
3280
+ value: clause.value,
3281
+ forceFreeform: isRegex,
3282
+ placeholder: isRegex ? "regex" : "value",
3283
+ classNames,
3284
+ onChange: (v) => onChange({ ...clause, value: v })
3285
+ }
3286
+ ),
3287
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-chip-x", onClick: onRemove, disabled, children: "\u2715" })
2203
3288
  ] });
2204
3289
  }
2205
3290
  function ValueInput({
2206
3291
  api,
2207
3292
  field,
2208
3293
  value,
3294
+ forceFreeform,
3295
+ placeholder = "value",
2209
3296
  classNames,
2210
3297
  onChange
2211
3298
  }) {
3299
+ const presentation = useFilterValuePresentation();
2212
3300
  const strategy = field ? filterValuesFor(field) : { source: "freeform" };
2213
- if (strategy.source === "static") {
2214
- const selectedText = value || "\u2014";
2215
- const widthChars = chipFieldWidthForSelect(selectedText, 4);
2216
- return /* @__PURE__ */ jsxs5(
2217
- "select",
2218
- {
2219
- className: cx3("qt-chip-val", classNames?.select),
2220
- value,
2221
- style: { width: `${widthChars}ch` },
2222
- onChange: (e) => onChange(e.target.value),
2223
- children: [
2224
- /* @__PURE__ */ jsx5("option", { value: "", children: "\u2014" }),
2225
- strategy.options.map((o) => /* @__PURE__ */ jsx5("option", { value: o, children: o }, o))
2226
- ]
2227
- }
2228
- );
3301
+ if (!forceFreeform && strategy.source === "static") {
3302
+ if (presentation.render || presentation.label || strategy.options.some((o) => typeof o !== "string")) {
3303
+ return /* @__PURE__ */ jsx7(PresentedValueInput, { field: field?.name ?? "", value, options: strategy.options, onChange });
3304
+ }
3305
+ return /* @__PURE__ */ jsxs7("select", { className: cx3("qt-chip-val", classNames?.select), value, onChange: (e) => onChange(e.target.value), children: [
3306
+ /* @__PURE__ */ jsx7("option", { value: "", children: "\u2014" }),
3307
+ strategy.options.map((o) => {
3308
+ const option = typeof o === "string" ? { value: o, label: o } : o;
3309
+ return /* @__PURE__ */ jsx7("option", { value: option.value, children: option.label }, option.value);
3310
+ })
3311
+ ] });
2229
3312
  }
2230
- if (strategy.source === "freeform" || !field) {
2231
- const widthChars = chipFieldWidthForInput(value || "value", field?.type, 5, 4);
2232
- return /* @__PURE__ */ jsx5(
3313
+ if (forceFreeform || strategy.source === "freeform" || !field) {
3314
+ const widthChars = chipFieldWidthForInput(value || placeholder, field?.type, 5, 4);
3315
+ return /* @__PURE__ */ jsx7(
2233
3316
  "input",
2234
3317
  {
2235
3318
  className: cx3("qt-chip-val", classNames?.input),
2236
3319
  value,
2237
- placeholder: "value",
3320
+ placeholder,
2238
3321
  size: widthChars,
2239
3322
  style: { width: `${widthChars}ch` },
2240
3323
  onChange: (e) => onChange(e.target.value)
2241
3324
  }
2242
3325
  );
2243
3326
  }
2244
- return /* @__PURE__ */ jsx5(AutocompleteInput, { api, field, value, classNames, onChange });
3327
+ return /* @__PURE__ */ jsx7(AutocompleteInput, { api, field, value, classNames, onChange });
2245
3328
  }
2246
3329
  function AutocompleteInput({
2247
3330
  api,
@@ -2250,9 +3333,9 @@ function AutocompleteInput({
2250
3333
  classNames,
2251
3334
  onChange
2252
3335
  }) {
2253
- const [result, setResult] = useState4({ values: [], hasMore: false });
2254
- const listId = useId();
2255
- useEffect4(() => {
3336
+ const [result, setResult] = useState6({ values: [], hasMore: false });
3337
+ const listId = useId2();
3338
+ useEffect5(() => {
2256
3339
  let cancelled = false;
2257
3340
  const t = setTimeout(() => {
2258
3341
  void api.filterValues(field.name, value).then((r) => {
@@ -2266,8 +3349,8 @@ function AutocompleteInput({
2266
3349
  clearTimeout(t);
2267
3350
  };
2268
3351
  }, [api, field.name, value]);
2269
- return /* @__PURE__ */ jsxs5("span", { className: "qt-autocomplete", children: [
2270
- /* @__PURE__ */ jsx5(
3352
+ return /* @__PURE__ */ jsxs7("span", { className: "qt-autocomplete", children: [
3353
+ /* @__PURE__ */ jsx7(
2271
3354
  "input",
2272
3355
  {
2273
3356
  className: cx3("qt-chip-val", classNames?.input),
@@ -2279,8 +3362,8 @@ function AutocompleteInput({
2279
3362
  onChange: (e) => onChange(e.target.value)
2280
3363
  }
2281
3364
  ),
2282
- /* @__PURE__ */ jsx5("datalist", { id: listId, children: result.values.map((v) => /* @__PURE__ */ jsx5("option", { value: v }, v)) }),
2283
- result.hasMore && /* @__PURE__ */ jsx5("span", { className: "qt-autocomplete-hint", children: "keep typing to refine\u2026" })
3365
+ /* @__PURE__ */ jsx7("datalist", { id: listId, children: result.values.map((v) => /* @__PURE__ */ jsx7("option", { value: v }, v)) }),
3366
+ result.hasMore && /* @__PURE__ */ jsx7("span", { className: "qt-autocomplete-hint", children: "keep typing to refine\u2026" })
2284
3367
  ] });
2285
3368
  }
2286
3369
  function OrderTermChip({
@@ -2297,6 +3380,7 @@ function OrderTermChip({
2297
3380
  onDragEnd,
2298
3381
  disabled,
2299
3382
  updateTerm,
3383
+ clearExtract,
2300
3384
  removeTerm,
2301
3385
  position,
2302
3386
  count,
@@ -2305,7 +3389,7 @@ function OrderTermChip({
2305
3389
  label
2306
3390
  }) {
2307
3391
  const fieldHasNull = useFieldHasNull(api, field?.name);
2308
- return /* @__PURE__ */ jsxs5(
3392
+ return /* @__PURE__ */ jsxs7(
2309
3393
  "span",
2310
3394
  {
2311
3395
  className: cx3("qt-chip", "qt-chip--col", "qt-chip--sort", isDragSource && "qt-chip--dragging", classNames?.chip),
@@ -2323,10 +3407,10 @@ function OrderTermChip({
2323
3407
  onDragEnd,
2324
3408
  title: "drag to reorder sort priority",
2325
3409
  children: [
2326
- /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2327
- showPriority && /* @__PURE__ */ jsx5("span", { className: "qt-chip-priority", children: priority }),
2328
- /* @__PURE__ */ jsx5("span", { className: "qt-chip-field", children: label }),
2329
- /* @__PURE__ */ jsx5(
3410
+ /* @__PURE__ */ jsx7("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
3411
+ showPriority && /* @__PURE__ */ jsx7("span", { className: "qt-chip-priority", children: priority }),
3412
+ /* @__PURE__ */ jsx7("span", { className: "qt-chip-field", children: label }),
3413
+ /* @__PURE__ */ jsx7(
2330
3414
  "button",
2331
3415
  {
2332
3416
  type: "button",
@@ -2336,7 +3420,45 @@ function OrderTermChip({
2336
3420
  children: term.dir === "asc" ? "\u2191 asc" : "\u2193 desc"
2337
3421
  }
2338
3422
  ),
2339
- fieldHasNull !== false && /* @__PURE__ */ jsxs5(
3423
+ term.extract ? /* @__PURE__ */ jsxs7("span", { className: "qt-sort-extract", children: [
3424
+ /* @__PURE__ */ jsx7("span", { className: "qt-chip-and", children: "extract" }),
3425
+ /* @__PURE__ */ jsx7(
3426
+ "input",
3427
+ {
3428
+ className: cx3("qt-chip-val", classNames?.input),
3429
+ value: term.extract.regex,
3430
+ placeholder: "regex",
3431
+ "aria-label": `Regex extract for ${label}`,
3432
+ size: chipFieldWidthForInput(term.extract.regex || "regex", "text", 5, 4),
3433
+ style: { width: `${chipFieldWidthForInput(term.extract.regex || "regex", "text", 5, 4)}ch` },
3434
+ disabled,
3435
+ onChange: (e) => updateTerm(dataIndex, { extract: { regex: e.target.value } })
3436
+ }
3437
+ ),
3438
+ /* @__PURE__ */ jsx7(
3439
+ "button",
3440
+ {
3441
+ type: "button",
3442
+ className: "qt-chip-x",
3443
+ title: "Remove regex extraction",
3444
+ "aria-label": `Remove regex extract for ${label}`,
3445
+ disabled,
3446
+ onClick: () => clearExtract(dataIndex),
3447
+ children: "\u2715"
3448
+ }
3449
+ )
3450
+ ] }) : /* @__PURE__ */ jsx7(
3451
+ "button",
3452
+ {
3453
+ type: "button",
3454
+ className: "qt-chip-op-btn",
3455
+ title: "Sort by text extracted with a regular expression",
3456
+ disabled,
3457
+ onClick: () => updateTerm(dataIndex, { extract: { regex: "" } }),
3458
+ children: "+ regex extract"
3459
+ }
3460
+ ),
3461
+ (term.extract != null || fieldHasNull !== false) && /* @__PURE__ */ jsxs7(
2340
3462
  "button",
2341
3463
  {
2342
3464
  type: "button",
@@ -2350,7 +3472,7 @@ function OrderTermChip({
2350
3472
  ]
2351
3473
  }
2352
3474
  ),
2353
- /* @__PURE__ */ jsx5(
3475
+ /* @__PURE__ */ jsx7(
2354
3476
  MoveButtons,
2355
3477
  {
2356
3478
  label,
@@ -2360,7 +3482,7 @@ function OrderTermChip({
2360
3482
  onMove: moveTerm
2361
3483
  }
2362
3484
  ),
2363
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => removeTerm(dataIndex), disabled, children: "\u2715" })
3485
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-chip-x", onClick: () => removeTerm(dataIndex), disabled, children: "\u2715" })
2364
3486
  ]
2365
3487
  }
2366
3488
  );
@@ -2372,15 +3494,24 @@ function OrderRow({
2372
3494
  disabled
2373
3495
  }) {
2374
3496
  const orderBy = api.query.orderBy;
2375
- const byName = useMemo3(() => new Map(fields.map((f) => [f.name, f])), [fields]);
2376
- const [drag, setDrag] = useState4(null);
2377
- const [adding, setAdding] = useState4(false);
3497
+ const byName = useMemo4(() => new Map(fields.map((f) => [f.name, f])), [fields]);
3498
+ const [drag, setDrag] = useState6(null);
3499
+ const [adding, setAdding] = useState6(false);
2378
3500
  function setOrderBy(next) {
2379
3501
  api.setSort(next);
2380
3502
  }
2381
3503
  function updateTerm(i, patch) {
2382
3504
  setOrderBy(orderBy.map((o, k) => k === i ? { ...o, ...patch } : o));
2383
3505
  }
3506
+ function clearExtract(i) {
3507
+ setOrderBy(
3508
+ orderBy.map((term, index) => {
3509
+ if (index !== i) return term;
3510
+ const { extract: _extract, ...plainTerm } = term;
3511
+ return plainTerm;
3512
+ })
3513
+ );
3514
+ }
2384
3515
  function removeTerm(i) {
2385
3516
  setOrderBy(orderBy.filter((_, k) => k !== i));
2386
3517
  }
@@ -2406,7 +3537,7 @@ function OrderRow({
2406
3537
  setAdding(false);
2407
3538
  }
2408
3539
  const termFields = orderBy.map((o) => o.field);
2409
- const termByField = useMemo3(() => new Map(orderBy.map((o) => [o.field, o])), [orderBy]);
3540
+ const termByField = useMemo4(() => new Map(orderBy.map((o) => [o.field, o])), [orderBy]);
2410
3541
  function previewFields() {
2411
3542
  if (!drag) return termFields;
2412
3543
  const without = termFields.filter((f) => f !== drag.source);
@@ -2436,14 +3567,14 @@ function OrderRow({
2436
3567
  const sortable = fields.filter(isSortable2);
2437
3568
  const isDefaultOrderBy = orderBy.length === api.defaults.orderBy.length && orderBy.every((term, i) => {
2438
3569
  const next = api.defaults.orderBy[i];
2439
- return term.field === next?.field && term.dir === next?.dir && (term.nulls ?? "last") === (next?.nulls ?? "last");
3570
+ return term.field === next?.field && term.dir === next?.dir && (term.nulls ?? "last") === (next?.nulls ?? "last") && (term.extract?.regex ?? null) === (next?.extract?.regex ?? null);
2440
3571
  });
2441
- return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row", children: [
2442
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "order by" }),
2443
- orderBy.length === 0 && !adding && /* @__PURE__ */ jsx5("span", { className: "qt-qb-hint", children: "(default)" }),
3572
+ return /* @__PURE__ */ jsxs7("div", { className: "qt-qb-row", children: [
3573
+ /* @__PURE__ */ jsx7("span", { className: "qt-qb-kw", children: "order by" }),
3574
+ orderBy.length === 0 && !adding && /* @__PURE__ */ jsx7("span", { className: "qt-qb-hint", children: "(default)" }),
2444
3575
  rendered.map((f, pos) => {
2445
3576
  const term = termByField.get(f);
2446
- return /* @__PURE__ */ jsx5(
3577
+ return /* @__PURE__ */ jsx7(
2447
3578
  OrderTermChip,
2448
3579
  {
2449
3580
  api,
@@ -2459,6 +3590,7 @@ function OrderRow({
2459
3590
  onDragEnd: () => setDrag(null),
2460
3591
  disabled,
2461
3592
  updateTerm,
3593
+ clearExtract,
2462
3594
  removeTerm,
2463
3595
  position: pos,
2464
3596
  count: rendered.length,
@@ -2476,8 +3608,8 @@ function OrderRow({
2476
3608
  f
2477
3609
  );
2478
3610
  }),
2479
- adding ? /* @__PURE__ */ jsx5(FieldPicker, { fields: sortable, onPick: addTerm, onClose: () => setAdding(false) }) : /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add sort" }),
2480
- !isDefaultOrderBy && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: () => api.setSort(api.defaults.orderBy), disabled, children: "reset" })
3611
+ adding ? /* @__PURE__ */ jsx7(FieldPicker, { fields: sortable, onPick: addTerm, onClose: () => setAdding(false) }) : /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add sort" }),
3612
+ !isDefaultOrderBy && /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-link-btn", onClick: () => api.setSort(api.defaults.orderBy), disabled, children: "reset" })
2481
3613
  ] });
2482
3614
  }
2483
3615
  function WindowRow({
@@ -2486,10 +3618,10 @@ function WindowRow({
2486
3618
  disabled
2487
3619
  }) {
2488
3620
  const { query } = api;
2489
- const [limitDraft, setLimitDraft] = useState4(String(query.limit));
2490
- const [offsetDraft, setOffsetDraft] = useState4(String(query.offset));
2491
- useEffect4(() => setLimitDraft(String(query.limit)), [query.limit]);
2492
- useEffect4(() => setOffsetDraft(String(query.offset)), [query.offset]);
3621
+ const [limitDraft, setLimitDraft] = useState6(String(query.limit));
3622
+ const [offsetDraft, setOffsetDraft] = useState6(String(query.offset));
3623
+ useEffect5(() => setLimitDraft(String(query.limit)), [query.limit]);
3624
+ useEffect5(() => setOffsetDraft(String(query.offset)), [query.offset]);
2493
3625
  const hasLimitDefault = query.limit === api.defaults.limit;
2494
3626
  const hasOffsetDefault = query.offset === api.defaults.offset;
2495
3627
  const hasWindowDefault = hasLimitDefault && hasOffsetDefault;
@@ -2499,10 +3631,10 @@ function WindowRow({
2499
3631
  function commitOffset() {
2500
3632
  api.setOffset(Math.max(0, Number(offsetDraft) || 0));
2501
3633
  }
2502
- return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row qt-qb-pagination", children: [
2503
- /* @__PURE__ */ jsxs5("label", { className: "qt-qb-window-field", children: [
2504
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "limit" }),
2505
- /* @__PURE__ */ jsx5(
3634
+ return /* @__PURE__ */ jsxs7("div", { className: "qt-qb-row qt-qb-pagination", children: [
3635
+ /* @__PURE__ */ jsxs7("label", { className: "qt-qb-window-field", children: [
3636
+ /* @__PURE__ */ jsx7("span", { className: "qt-qb-kw", children: "limit" }),
3637
+ /* @__PURE__ */ jsx7(
2506
3638
  "input",
2507
3639
  {
2508
3640
  className: cx3("qt-qb-num", classNames?.input),
@@ -2516,9 +3648,9 @@ function WindowRow({
2516
3648
  }
2517
3649
  )
2518
3650
  ] }),
2519
- /* @__PURE__ */ jsxs5("label", { className: "qt-qb-window-field", children: [
2520
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "offset" }),
2521
- /* @__PURE__ */ jsx5(
3651
+ /* @__PURE__ */ jsxs7("label", { className: "qt-qb-window-field", children: [
3652
+ /* @__PURE__ */ jsx7("span", { className: "qt-qb-kw", children: "offset" }),
3653
+ /* @__PURE__ */ jsx7(
2522
3654
  "input",
2523
3655
  {
2524
3656
  className: cx3("qt-qb-num", classNames?.input),
@@ -2532,7 +3664,7 @@ function WindowRow({
2532
3664
  }
2533
3665
  )
2534
3666
  ] }),
2535
- !hasWindowDefault && /* @__PURE__ */ jsx5(
3667
+ !hasWindowDefault && /* @__PURE__ */ jsx7(
2536
3668
  "button",
2537
3669
  {
2538
3670
  type: "button",
@@ -2565,11 +3697,11 @@ function MetricsRow({
2565
3697
  }) {
2566
3698
  const { aggregations } = api;
2567
3699
  const clauses = aggregations.clauses;
2568
- const measurable = useMemo3(() => fields.filter(isMeasurable), [fields]);
2569
- const groupable = useMemo3(() => fields.filter(isGroupable), [fields]);
2570
- const [drag, setDrag] = useState4(null);
3700
+ const measurable = useMemo4(() => fields.filter(isMeasurable), [fields]);
3701
+ const groupable = useMemo4(() => fields.filter(isGroupable), [fields]);
3702
+ const [drag, setDrag] = useState6(null);
2571
3703
  const ids = clauses.map((c) => c.id);
2572
- const byId = useMemo3(() => new Map(clauses.map((c) => [c.id, c])), [clauses]);
3704
+ const byId = useMemo4(() => new Map(clauses.map((c) => [c.id, c])), [clauses]);
2573
3705
  function previewIds() {
2574
3706
  if (!drag) return ids;
2575
3707
  const without = ids.filter((i) => i !== drag.source);
@@ -2595,12 +3727,12 @@ function MetricsRow({
2595
3727
  setDrag(null);
2596
3728
  }
2597
3729
  const rendered = previewIds();
2598
- return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row qt-qb-row--metrics", children: [
2599
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "metrics" }),
2600
- clauses.length === 0 && /* @__PURE__ */ jsx5("span", { className: "qt-qb-hint", children: "none" }),
3730
+ return /* @__PURE__ */ jsxs7("div", { className: "qt-qb-row qt-qb-row--metrics", children: [
3731
+ /* @__PURE__ */ jsx7("span", { className: "qt-qb-kw", children: "metrics" }),
3732
+ clauses.length === 0 && /* @__PURE__ */ jsx7("span", { className: "qt-qb-hint", children: "none" }),
2601
3733
  rendered.map((id, position) => {
2602
3734
  const clause = byId.get(id);
2603
- return /* @__PURE__ */ jsx5(
3735
+ return /* @__PURE__ */ jsx7(
2604
3736
  MetricChip,
2605
3737
  {
2606
3738
  clause,
@@ -2625,8 +3757,8 @@ function MetricsRow({
2625
3757
  id
2626
3758
  );
2627
3759
  }),
2628
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-add", onClick: () => aggregations.add(), disabled, children: "+ add metric" }),
2629
- clauses.length > 0 && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: aggregations.clear, disabled, title: "Remove all metrics", children: "reset" })
3760
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-add", onClick: () => aggregations.add(), disabled, children: "+ add metric" }),
3761
+ clauses.length > 0 && /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-link-btn", onClick: aggregations.clear, disabled, title: "Remove all metrics", children: "reset" })
2630
3762
  ] });
2631
3763
  }
2632
3764
  function changeMetricOp(aggregations, clause, measurable, op) {
@@ -2655,11 +3787,11 @@ function MetricChip({
2655
3787
  count,
2656
3788
  onMove
2657
3789
  }) {
2658
- const [addingGroup, setAddingGroup] = useState4(false);
2659
- const byName = useMemo3(() => new Map(groupable.map((f) => [f.name, f])), [groupable]);
3790
+ const [addingGroup, setAddingGroup] = useState6(false);
3791
+ const byName = useMemo4(() => new Map(groupable.map((f) => [f.name, f])), [groupable]);
2660
3792
  const needsField = aggOpNeedsField(clause.op);
2661
3793
  const measureOptions = measurable.filter((f) => aggOpsForField(f).includes(clause.op));
2662
- return /* @__PURE__ */ jsxs5(
3794
+ return /* @__PURE__ */ jsxs7(
2663
3795
  "span",
2664
3796
  {
2665
3797
  className: cx3("qt-chip", "qt-chip--agg", isDragSource && "qt-chip--dragging", classNames?.chip),
@@ -2677,18 +3809,18 @@ function MetricChip({
2677
3809
  onDragEnd,
2678
3810
  title: "drag to reorder metrics",
2679
3811
  children: [
2680
- /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2681
- /* @__PURE__ */ jsx5(
3812
+ /* @__PURE__ */ jsx7("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
3813
+ /* @__PURE__ */ jsx7(
2682
3814
  "select",
2683
3815
  {
2684
3816
  className: cx3("qt-chip-op", classNames?.select),
2685
3817
  value: clause.op,
2686
3818
  disabled,
2687
3819
  onChange: (e) => onChangeOp(e.target.value),
2688
- children: AGG_OPS.map((op) => /* @__PURE__ */ jsx5("option", { value: op, children: AGG_OP_LABELS[op] }, op))
3820
+ children: AGG_OPS.map((op) => /* @__PURE__ */ jsx7("option", { value: op, children: AGG_OP_LABELS[op] }, op))
2689
3821
  }
2690
3822
  ),
2691
- /* @__PURE__ */ jsxs5(
3823
+ /* @__PURE__ */ jsxs7(
2692
3824
  "select",
2693
3825
  {
2694
3826
  className: cx3("qt-chip-val", classNames?.select),
@@ -2696,18 +3828,18 @@ function MetricChip({
2696
3828
  disabled,
2697
3829
  onChange: (e) => onChangeField(e.target.value || void 0),
2698
3830
  children: [
2699
- !needsField && /* @__PURE__ */ jsx5("option", { value: "", children: "(all rows)" }),
2700
- needsField && clause.field == null && /* @__PURE__ */ jsx5("option", { value: "", children: "measure\u2026" }),
2701
- measureOptions.map((f) => /* @__PURE__ */ jsx5("option", { value: f.name, children: f.label }, f.name))
3831
+ !needsField && /* @__PURE__ */ jsx7("option", { value: "", children: "(all rows)" }),
3832
+ needsField && clause.field == null && /* @__PURE__ */ jsx7("option", { value: "", children: "measure\u2026" }),
3833
+ measureOptions.map((f) => /* @__PURE__ */ jsx7("option", { value: f.name, children: f.label }, f.name))
2702
3834
  ]
2703
3835
  }
2704
3836
  ),
2705
- /* @__PURE__ */ jsxs5("span", { className: "qt-chip-agg-by", children: [
2706
- clause.groupBy.map((g) => /* @__PURE__ */ jsxs5("span", { className: "qt-chip-agg-group", children: [
2707
- /* @__PURE__ */ jsx5("span", { className: "qt-chip-agg-group-label", children: byName.get(g)?.label ?? g }),
2708
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => onRemoveGroup(g), disabled, children: "\u2715" })
3837
+ /* @__PURE__ */ jsxs7("span", { className: "qt-chip-agg-by", children: [
3838
+ clause.groupBy.map((g) => /* @__PURE__ */ jsxs7("span", { className: "qt-chip-agg-group", children: [
3839
+ /* @__PURE__ */ jsx7("span", { className: "qt-chip-agg-group-label", children: byName.get(g)?.label ?? g }),
3840
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-chip-x", onClick: () => onRemoveGroup(g), disabled, children: "\u2715" })
2709
3841
  ] }, g)),
2710
- addingGroup ? /* @__PURE__ */ jsx5(
3842
+ addingGroup ? /* @__PURE__ */ jsx7(
2711
3843
  FieldPicker,
2712
3844
  {
2713
3845
  fields: groupable,
@@ -2718,7 +3850,7 @@ function MetricChip({
2718
3850
  },
2719
3851
  onClose: () => setAddingGroup(false)
2720
3852
  }
2721
- ) : /* @__PURE__ */ jsx5(
3853
+ ) : /* @__PURE__ */ jsx7(
2722
3854
  "button",
2723
3855
  {
2724
3856
  type: "button",
@@ -2730,7 +3862,7 @@ function MetricChip({
2730
3862
  }
2731
3863
  )
2732
3864
  ] }),
2733
- /* @__PURE__ */ jsx5(
3865
+ /* @__PURE__ */ jsx7(
2734
3866
  MoveButtons,
2735
3867
  {
2736
3868
  label: clause.label ?? `${AGG_OP_LABELS[clause.op]} metric`,
@@ -2740,16 +3872,16 @@ function MetricChip({
2740
3872
  onMove
2741
3873
  }
2742
3874
  ),
2743
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: onRemove, disabled, title: "Remove metric", children: "\u2715" })
3875
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "qt-chip-x", onClick: onRemove, disabled, title: "Remove metric", children: "\u2715" })
2744
3876
  ]
2745
3877
  }
2746
3878
  );
2747
3879
  }
2748
3880
 
2749
3881
  // src/MetricsPanel.tsx
2750
- import { useMemo as useMemo4 } from "react";
3882
+ import { useMemo as useMemo5 } from "react";
2751
3883
  import { EMPTY_QUERY } from "@pythia-software/query-table-core";
2752
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3884
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2753
3885
  var cx4 = (...parts) => parts.filter((p) => Boolean(p)).join(" ");
2754
3886
  var OP_LABELS = {
2755
3887
  count: "Count",
@@ -2767,18 +3899,18 @@ function MetricsPanel({
2767
3899
  emptyMessage,
2768
3900
  classNames
2769
3901
  }) {
2770
- const byName = useMemo4(() => new Map(fields.map((f) => [f.name, f])), [fields]);
3902
+ const byName = useMemo5(() => new Map(fields.map((f) => [f.name, f])), [fields]);
2771
3903
  const { clauses, results, loading, error } = aggregations;
2772
3904
  if (clauses.length === 0) {
2773
- return emptyMessage ? /* @__PURE__ */ jsx6("div", { className: cx4("qt-metrics", classNames?.root), children: emptyMessage }) : null;
3905
+ return emptyMessage ? /* @__PURE__ */ jsx8("div", { className: cx4("qt-metrics", classNames?.root), children: emptyMessage }) : null;
2774
3906
  }
2775
3907
  const resultById = new Map((results?.metrics ?? []).map((m) => [m.id, m.buckets]));
2776
- return /* @__PURE__ */ jsxs6("div", { className: cx4("qt-metrics", classNames?.root), children: [
2777
- error ? /* @__PURE__ */ jsxs6("div", { className: "qt-metrics-error", children: [
3908
+ return /* @__PURE__ */ jsxs8("div", { className: cx4("qt-metrics", classNames?.root), children: [
3909
+ error ? /* @__PURE__ */ jsxs8("div", { className: "qt-metrics-error", children: [
2778
3910
  "Metrics failed: ",
2779
3911
  error.message
2780
3912
  ] }) : null,
2781
- clauses.map((clause) => /* @__PURE__ */ jsx6(
3913
+ clauses.map((clause) => /* @__PURE__ */ jsx8(
2782
3914
  MetricCard,
2783
3915
  {
2784
3916
  clause,
@@ -2802,23 +3934,23 @@ function MetricCard({
2802
3934
  }) {
2803
3935
  const measure = clause.field ? byName.get(clause.field) : void 0;
2804
3936
  const groupLabels = clause.groupBy.map((g) => byName.get(g)?.label ?? g);
2805
- const decimals = useMemo4(() => cardDecimals(buckets), [buckets]);
3937
+ const decimals = useMemo5(() => cardDecimals(buckets), [buckets]);
2806
3938
  const fmt = (v) => formatMetricValue(v, clause.op, measure, renderers, decimals);
2807
- return /* @__PURE__ */ jsxs6("div", { className: cx4("qt-metric", `qt-metric--gb${Math.min(clause.groupBy.length, 3)}`, className), children: [
2808
- /* @__PURE__ */ jsxs6("div", { className: "qt-metric-head", children: [
2809
- /* @__PURE__ */ jsx6("span", { className: "qt-metric-title", children: metricTitle(clause, measure) }),
2810
- groupLabels.length > 0 && /* @__PURE__ */ jsxs6("span", { className: "qt-metric-by", children: [
3939
+ return /* @__PURE__ */ jsxs8("div", { className: cx4("qt-metric", `qt-metric--gb${Math.min(clause.groupBy.length, 3)}`, className), children: [
3940
+ /* @__PURE__ */ jsxs8("div", { className: "qt-metric-head", children: [
3941
+ /* @__PURE__ */ jsx8("span", { className: "qt-metric-title", children: metricTitle(clause, measure) }),
3942
+ groupLabels.length > 0 && /* @__PURE__ */ jsxs8("span", { className: "qt-metric-by", children: [
2811
3943
  "by ",
2812
3944
  groupLabels.join(" \xD7 ")
2813
3945
  ] })
2814
3946
  ] }),
2815
- loading && buckets.length === 0 ? /* @__PURE__ */ jsx6("div", { className: "qt-metric-loading", children: "\u2026" }) : buckets.length === 0 ? /* @__PURE__ */ jsx6("div", { className: "qt-metric-empty", children: "no data" }) : clause.groupBy.length === 0 ? /* @__PURE__ */ jsx6(BigNumber, { bucket: buckets[0], fmt }) : clause.groupBy.length === 1 ? /* @__PURE__ */ jsx6(BarList, { buckets, fmt }) : clause.groupBy.length === 2 ? /* @__PURE__ */ jsx6(Pivot, { buckets, fmt, rowLabel: groupLabels[0], colLabel: groupLabels[1] }) : /* @__PURE__ */ jsx6(FlatTable, { buckets, fmt, groupLabels, valueLabel: metricTitle(clause, measure) })
3947
+ loading && buckets.length === 0 ? /* @__PURE__ */ jsx8("div", { className: "qt-metric-loading", children: "\u2026" }) : buckets.length === 0 ? /* @__PURE__ */ jsx8("div", { className: "qt-metric-empty", children: "no data" }) : clause.groupBy.length === 0 ? /* @__PURE__ */ jsx8(BigNumber, { bucket: buckets[0], fmt }) : clause.groupBy.length === 1 ? /* @__PURE__ */ jsx8(BarList, { buckets, fmt }) : clause.groupBy.length === 2 ? /* @__PURE__ */ jsx8(Pivot, { buckets, fmt, rowLabel: groupLabels[0], colLabel: groupLabels[1] }) : /* @__PURE__ */ jsx8(FlatTable, { buckets, fmt, groupLabels, valueLabel: metricTitle(clause, measure) })
2816
3948
  ] });
2817
3949
  }
2818
3950
  function BigNumber({ bucket, fmt }) {
2819
- return /* @__PURE__ */ jsxs6("div", { className: "qt-metric-big", children: [
2820
- /* @__PURE__ */ jsx6("span", { className: "qt-metric-big-value", children: fmt(bucket.value) }),
2821
- /* @__PURE__ */ jsxs6("span", { className: "qt-metric-big-sub", children: [
3951
+ return /* @__PURE__ */ jsxs8("div", { className: "qt-metric-big", children: [
3952
+ /* @__PURE__ */ jsx8("span", { className: "qt-metric-big-value", children: fmt(bucket.value) }),
3953
+ /* @__PURE__ */ jsxs8("span", { className: "qt-metric-big-sub", children: [
2822
3954
  bucket.count.toLocaleString(),
2823
3955
  " rows"
2824
3956
  ] })
@@ -2826,12 +3958,12 @@ function BigNumber({ bucket, fmt }) {
2826
3958
  }
2827
3959
  function BarList({ buckets, fmt }) {
2828
3960
  const max = Math.max(...buckets.map((b) => barMagnitude(b)), 0);
2829
- return /* @__PURE__ */ jsx6("div", { className: "qt-metric-bars", children: buckets.map((b, i) => {
3961
+ return /* @__PURE__ */ jsx8("div", { className: "qt-metric-bars", children: buckets.map((b, i) => {
2830
3962
  const pct = max > 0 ? barMagnitude(b) / max * 100 : 0;
2831
- return /* @__PURE__ */ jsxs6("div", { className: "qt-metric-bar-row", children: [
2832
- /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-label", title: keyText(b.keys[0]), children: keyNode(b.keys[0]) }),
2833
- /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-track", children: /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-fill", style: { width: `${pct}%` } }) }),
2834
- /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-value", children: fmt(b.value) })
3963
+ return /* @__PURE__ */ jsxs8("div", { className: "qt-metric-bar-row", children: [
3964
+ /* @__PURE__ */ jsx8("span", { className: "qt-metric-bar-label", title: keyText(b.keys[0]), children: keyNode(b.keys[0]) }),
3965
+ /* @__PURE__ */ jsx8("span", { className: "qt-metric-bar-track", children: /* @__PURE__ */ jsx8("span", { className: "qt-metric-bar-fill", style: { width: `${pct}%` } }) }),
3966
+ /* @__PURE__ */ jsx8("span", { className: "qt-metric-bar-value", children: fmt(b.value) })
2835
3967
  ] }, `${keyText(b.keys[0])}-${i}`);
2836
3968
  }) });
2837
3969
  }
@@ -2845,22 +3977,22 @@ function Pivot({
2845
3977
  const colKeys = distinctKeys(buckets, 1);
2846
3978
  const cell = /* @__PURE__ */ new Map();
2847
3979
  for (const b of buckets) cell.set(`${keyText(b.keys[0])}\0${keyText(b.keys[1])}`, b.value);
2848
- return /* @__PURE__ */ jsx6("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs6("table", { className: "qt-metric-pivot", children: [
2849
- /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs6("tr", { children: [
2850
- /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-corner", children: /* @__PURE__ */ jsxs6("span", { className: "qt-metric-pivot-axes", children: [
3980
+ return /* @__PURE__ */ jsx8("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs8("table", { className: "qt-metric-pivot", children: [
3981
+ /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsxs8("tr", { children: [
3982
+ /* @__PURE__ */ jsx8("th", { className: "qt-metric-pivot-corner", children: /* @__PURE__ */ jsxs8("span", { className: "qt-metric-pivot-axes", children: [
2851
3983
  rowLabel,
2852
3984
  " ",
2853
- /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\\" }),
3985
+ /* @__PURE__ */ jsx8("span", { className: "qt-muted", children: "\\" }),
2854
3986
  " ",
2855
3987
  colLabel
2856
3988
  ] }) }),
2857
- colKeys.map((c) => /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-col", children: keyNode(c) }, keyText(c)))
3989
+ colKeys.map((c) => /* @__PURE__ */ jsx8("th", { className: "qt-metric-pivot-col", children: keyNode(c) }, keyText(c)))
2858
3990
  ] }) }),
2859
- /* @__PURE__ */ jsx6("tbody", { children: rowKeys.map((r) => /* @__PURE__ */ jsxs6("tr", { children: [
2860
- /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-row", children: keyNode(r) }),
3991
+ /* @__PURE__ */ jsx8("tbody", { children: rowKeys.map((r) => /* @__PURE__ */ jsxs8("tr", { children: [
3992
+ /* @__PURE__ */ jsx8("th", { className: "qt-metric-pivot-row", children: keyNode(r) }),
2861
3993
  colKeys.map((c) => {
2862
3994
  const v = cell.get(`${keyText(r)}\0${keyText(c)}`);
2863
- return /* @__PURE__ */ jsx6("td", { className: "qt-metric-pivot-cell", children: v === void 0 ? /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\xB7" }) : fmt(v) }, keyText(c));
3995
+ return /* @__PURE__ */ jsx8("td", { className: "qt-metric-pivot-cell", children: v === void 0 ? /* @__PURE__ */ jsx8("span", { className: "qt-muted", children: "\xB7" }) : fmt(v) }, keyText(c));
2864
3996
  })
2865
3997
  ] }, keyText(r))) })
2866
3998
  ] }) });
@@ -2869,16 +4001,16 @@ function FlatTable({
2869
4001
  buckets,
2870
4002
  fmt,
2871
4003
  groupLabels,
2872
- valueLabel
4004
+ valueLabel: valueLabel2
2873
4005
  }) {
2874
- return /* @__PURE__ */ jsx6("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs6("table", { className: "qt-metric-flat", children: [
2875
- /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs6("tr", { children: [
2876
- groupLabels.map((g) => /* @__PURE__ */ jsx6("th", { children: g }, g)),
2877
- /* @__PURE__ */ jsx6("th", { className: "qt-metric-flat-value", children: valueLabel })
4006
+ return /* @__PURE__ */ jsx8("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs8("table", { className: "qt-metric-flat", children: [
4007
+ /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsxs8("tr", { children: [
4008
+ groupLabels.map((g) => /* @__PURE__ */ jsx8("th", { children: g }, g)),
4009
+ /* @__PURE__ */ jsx8("th", { className: "qt-metric-flat-value", children: valueLabel2 })
2878
4010
  ] }) }),
2879
- /* @__PURE__ */ jsx6("tbody", { children: buckets.map((b, i) => /* @__PURE__ */ jsxs6("tr", { children: [
2880
- groupLabels.map((_, j) => /* @__PURE__ */ jsx6("td", { children: keyNode(b.keys[j]) }, j)),
2881
- /* @__PURE__ */ jsx6("td", { className: "qt-metric-flat-value", children: fmt(b.value) })
4011
+ /* @__PURE__ */ jsx8("tbody", { children: buckets.map((b, i) => /* @__PURE__ */ jsxs8("tr", { children: [
4012
+ groupLabels.map((_, j) => /* @__PURE__ */ jsx8("td", { children: keyNode(b.keys[j]) }, j)),
4013
+ /* @__PURE__ */ jsx8("td", { className: "qt-metric-flat-value", children: fmt(b.value) })
2882
4014
  ] }, i)) })
2883
4015
  ] }) });
2884
4016
  }
@@ -2889,7 +4021,7 @@ function metricTitle(clause, measure) {
2889
4021
  }
2890
4022
  var UNIT_RENDER_KEYS = /* @__PURE__ */ new Set(["duration_ms", "byte_size"]);
2891
4023
  function formatMetricValue(value, op, measure, renderers, decimals) {
2892
- if (value == null) return /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\u2014" });
4024
+ if (value == null) return /* @__PURE__ */ jsx8("span", { className: "qt-muted", children: "\u2014" });
2893
4025
  if (op === "count" || op === "count_distinct") {
2894
4026
  return typeof value === "number" ? formatNumber(value, 0) : String(value);
2895
4027
  }
@@ -2952,21 +4084,21 @@ function keyText(k) {
2952
4084
  return k == null ? NULL_KEY : String(k);
2953
4085
  }
2954
4086
  function keyNode(k) {
2955
- return k == null ? /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: NULL_KEY }) : String(k);
4087
+ return k == null ? /* @__PURE__ */ jsx8("span", { className: "qt-muted", children: NULL_KEY }) : String(k);
2956
4088
  }
2957
4089
 
2958
4090
  // src/SelectionToolbar.tsx
2959
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
4091
+ import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
2960
4092
  var cx5 = (...parts) => parts.filter(Boolean).join(" ");
2961
4093
  function SelectionToolbar({ selection, actions, alwaysShow, className }) {
2962
4094
  if (selection.count === 0 && !alwaysShow) return null;
2963
4095
  const selectedIds = [...selection.selected];
2964
- return /* @__PURE__ */ jsxs7("div", { className: cx5("qt-selection-toolbar", className), role: "toolbar", children: [
2965
- /* @__PURE__ */ jsxs7("span", { className: "qt-selection-count", children: [
4096
+ return /* @__PURE__ */ jsxs9("div", { className: cx5("qt-selection-toolbar", className), role: "toolbar", children: [
4097
+ /* @__PURE__ */ jsxs9("span", { className: "qt-selection-count", children: [
2966
4098
  selection.count,
2967
4099
  " selected"
2968
4100
  ] }),
2969
- /* @__PURE__ */ jsx7(
4101
+ /* @__PURE__ */ jsx9(
2970
4102
  "button",
2971
4103
  {
2972
4104
  type: "button",
@@ -2976,16 +4108,19 @@ function SelectionToolbar({ selection, actions, alwaysShow, className }) {
2976
4108
  children: "clear"
2977
4109
  }
2978
4110
  ),
2979
- /* @__PURE__ */ jsx7("span", { className: "qt-selection-actions", children: actions(selectedIds) })
4111
+ /* @__PURE__ */ jsx9("span", { className: "qt-selection-actions", children: actions(selectedIds, selection) })
2980
4112
  ] });
2981
4113
  }
2982
4114
  export {
2983
4115
  CellMenu,
2984
4116
  DataTable,
2985
4117
  FieldPicker,
4118
+ FilterValueProvider,
2986
4119
  MetricsPanel,
4120
+ PresentedFilterValue,
2987
4121
  QueryBuilder,
2988
4122
  SavedQueriesModal,
4123
+ SelectColumnEditor,
2989
4124
  SelectionToolbar,
2990
4125
  defaultRenderers,
2991
4126
  resolveRenderer,