@pythia-software/query-table-ui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -137,6 +137,7 @@ function formatRelative(iso) {
137
137
  }
138
138
 
139
139
  // src/DataTable.tsx
140
+ import { isComputedCellError } from "@pythia-software/query-table-core";
140
141
  import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
141
142
  import { useVirtualizer } from "@tanstack/react-virtual";
142
143
  import { isSortable, readFieldValue } from "@pythia-software/query-table-core";
@@ -144,10 +145,10 @@ import { useColumnDrag } from "@pythia-software/query-table-react";
144
145
 
145
146
  // src/CellMenu.tsx
146
147
  import { useEffect } from "react";
147
- import { NULLARY_OPS, opsForField } from "@pythia-software/query-table-core";
148
+ import { NULLARY_OPS, negateClause, opsForField, opPairsForField, isFilterable } from "@pythia-software/query-table-core";
148
149
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
149
150
  var cx = (...parts) => parts.filter(Boolean).join(" ");
150
- var MENU_WIDTH = 240;
151
+ var MENU_WIDTH = 320;
151
152
  var ROW_HEIGHT = 32;
152
153
  var POINTER_OFFSET = 4;
153
154
  function CellMenu({ field, value, x, y, onAddFilter, onClose, classNames }) {
@@ -166,73 +167,36 @@ function CellMenu({ field, value, x, y, onAddFilter, onClose, classNames }) {
166
167
  clearTimeout(t);
167
168
  };
168
169
  }, [onClose]);
169
- const ops = opsForField(field);
170
+ const ops = isFilterable(field) ? opsForField(field) : [];
171
+ const pairs = isFilterable(field) ? opPairsForField(field) : [];
172
+ const hasNullity = pairs.some((p) => p.keep.op === "is_null");
170
173
  const isArray = Array.isArray(value);
171
174
  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 });
175
+ const cellText = valueToString(value);
176
+ function applyClause(clause) {
177
+ onAddFilter(clause);
175
178
  onClose();
176
179
  }
177
- const filterItems = [];
180
+ const rows = [];
181
+ const pairRow = (op, raw) => {
182
+ const positive = { field: field.name, op, value: NULLARY_OPS.has(op) ? "" : raw };
183
+ rows.push({ positive, negative: negateClause(positive, ops) });
184
+ };
178
185
  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
- });
186
+ const arrayOp = pairs.find((p) => p.keep.op === "includes")?.keep.op ?? pairs[0]?.keep.op ?? "=";
187
+ value.forEach((el) => pairRow(arrayOp, String(el)));
188
+ if (hasNullity) pairRow("is_null", cellText);
189
+ } else if (!isNullish) {
190
+ for (const pair of pairs) pairRow(pair.keep.op, cellText);
191
+ } else if (hasNullity) {
192
+ pairRow("is_null", cellText);
191
193
  }
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
194
  const vw = typeof window !== "undefined" ? window.innerWidth : 9999;
232
195
  const vh = typeof window !== "undefined" ? window.innerHeight : 9999;
196
+ const approxRows = 3 + rows.length;
233
197
  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(
198
+ const top = Math.max(8, Math.min(y + POINTER_OFFSET, vh - ROW_HEIGHT * approxRows - 8));
199
+ return /* @__PURE__ */ jsxs(
236
200
  "div",
237
201
  {
238
202
  className: cx("qt-cell-menu", classNames?.popover),
@@ -240,7 +204,69 @@ function CellMenu({ field, value, x, y, onAddFilter, onClose, classNames }) {
240
204
  onClick: (e) => e.stopPropagation(),
241
205
  onContextMenu: (e) => e.preventDefault(),
242
206
  role: "menu",
243
- children: items
207
+ children: [
208
+ /* @__PURE__ */ jsxs("div", { className: "qt-cm-header", children: [
209
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-field", children: field.label }),
210
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-value", children: previewValue(value) })
211
+ ] }),
212
+ /* @__PURE__ */ jsx(
213
+ MenuItem,
214
+ {
215
+ classNames,
216
+ onClick: async () => {
217
+ try {
218
+ await navigator.clipboard.writeText(cellText);
219
+ } catch {
220
+ }
221
+ onClose();
222
+ },
223
+ children: "copy value"
224
+ }
225
+ ),
226
+ /* @__PURE__ */ jsx("div", { className: cx("qt-cm-sep", classNames?.separator) }),
227
+ rows.length > 0 && /* @__PURE__ */ jsxs("div", { className: "qt-cm-filter-grid", role: "group", "aria-label": "Filter by this cell", children: [
228
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-col-head", children: "keep" }),
229
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-col-head qt-cm-col-head--neg", children: "exclude" }),
230
+ rows.map((row, i) => /* @__PURE__ */ jsx(FilterPairRow, { row, value, classNames, onApply: applyClause }, i))
231
+ ] })
232
+ ]
233
+ }
234
+ );
235
+ }
236
+ function FilterPairRow({
237
+ row,
238
+ value,
239
+ classNames,
240
+ onApply
241
+ }) {
242
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
243
+ /* @__PURE__ */ jsx(FilterButton, { clause: row.positive, value, classNames, onApply }),
244
+ /* @__PURE__ */ jsx(FilterButton, { clause: row.negative, value, negative: true, classNames, onApply })
245
+ ] });
246
+ }
247
+ function FilterButton({
248
+ clause,
249
+ value,
250
+ negative,
251
+ classNames,
252
+ onApply
253
+ }) {
254
+ if (!clause) return /* @__PURE__ */ jsx("span", { className: "qt-cm-filter-empty", "aria-hidden": true });
255
+ const nullary = NULLARY_OPS.has(clause.op);
256
+ return /* @__PURE__ */ jsxs(
257
+ "button",
258
+ {
259
+ type: "button",
260
+ className: cx("qt-cm-item", "qt-cm-filter", negative && "qt-cm-filter--neg", classNames?.item),
261
+ onClick: () => onApply(clause),
262
+ role: "menuitem",
263
+ children: [
264
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-op", children: predicateSymbol(clause) }),
265
+ !nullary ? /* @__PURE__ */ jsxs(Fragment, { children: [
266
+ " ",
267
+ /* @__PURE__ */ jsx("code", { children: previewValue(value) })
268
+ ] }) : null
269
+ ]
244
270
  }
245
271
  );
246
272
  }
@@ -251,6 +277,10 @@ function MenuItem({
251
277
  }) {
252
278
  return /* @__PURE__ */ jsx("button", { type: "button", className: cx("qt-cm-item", classNames?.item), onClick, role: "menuitem", children });
253
279
  }
280
+ function predicateSymbol(clause) {
281
+ const base = opSymbol(clause.op);
282
+ return clause.negated ? `not ${base}` : base;
283
+ }
254
284
  function opSymbol(op) {
255
285
  switch (op) {
256
286
  case "!=":
@@ -259,6 +289,14 @@ function opSymbol(op) {
259
289
  return "\u2265";
260
290
  case "<=":
261
291
  return "\u2264";
292
+ case "starts_with":
293
+ return "starts with";
294
+ case "ends_with":
295
+ return "ends with";
296
+ case "matches_regex":
297
+ return "matches";
298
+ case "not_matches_regex":
299
+ return "not matches";
262
300
  case "is_null":
263
301
  return "is null";
264
302
  case "is_not_null":
@@ -936,7 +974,7 @@ function DataTable(props) {
936
974
  onClick: (e) => openMenu(e, f, row),
937
975
  onContextMenu: (e) => openMenu(e, f, row),
938
976
  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 })
977
+ children: copiedCellKey === key ? /* @__PURE__ */ jsx2("span", { className: "qt-cell-copy-chip", children: "\u2713 copied" }) : isComputedCellError(value) ? /* @__PURE__ */ jsx2("span", { className: "qt-formula-error", title: value.computedError, children: value.computedError === "Calculating\u2026" ? "Calculating\u2026" : "Error" }) : render({ value, row, field: f, query })
940
978
  },
941
979
  f.name
942
980
  );
@@ -1114,8 +1152,783 @@ function valueToString2(value) {
1114
1152
  return String(value);
1115
1153
  }
1116
1154
 
1155
+ // src/SelectColumnEditor.tsx
1156
+ import {
1157
+ lazy,
1158
+ Suspense,
1159
+ useEffect as useEffect3,
1160
+ useId,
1161
+ useMemo as useMemo2,
1162
+ useRef as useRef2,
1163
+ useState as useState3
1164
+ } from "react";
1165
+ import {
1166
+ computedFieldName,
1167
+ FORMULA_FUNCTIONS,
1168
+ isComputedField,
1169
+ isSelectable
1170
+ } from "@pythia-software/query-table-core";
1171
+
1172
+ // src/previewOrdering.ts
1173
+ var collator = new Intl.Collator(void 0, {
1174
+ numeric: true,
1175
+ sensitivity: "base"
1176
+ });
1177
+ var valueLabel = (value) => value === null ? "NULL" : typeof value === "string" ? value : JSON.stringify(value);
1178
+ function mixedRank(group) {
1179
+ const value = JSON.stringify([
1180
+ group.inputs,
1181
+ group.result.value,
1182
+ group.result.error ?? null
1183
+ ]);
1184
+ let hash = 2166136261;
1185
+ for (let i = 0; i < value.length; i++)
1186
+ hash = Math.imul(hash ^ value.charCodeAt(i), 16777619);
1187
+ return hash >>> 0;
1188
+ }
1189
+ function compareValue(a, b) {
1190
+ return collator.compare(valueLabel(a), valueLabel(b));
1191
+ }
1192
+ function sortValue(group, key) {
1193
+ switch (key.kind) {
1194
+ case "input":
1195
+ return group.inputs[key.index] ?? null;
1196
+ case "result":
1197
+ return group.result.error ?? group.result.value;
1198
+ case "count":
1199
+ case "percentage":
1200
+ return group.count;
1201
+ }
1202
+ }
1203
+ function orderPreviewGroups(groups, sort) {
1204
+ return groups.map((group, index) => ({ group, index })).sort((a, b) => {
1205
+ if (!sort) {
1206
+ const rank = mixedRank(a.group) - mixedRank(b.group);
1207
+ return rank || a.index - b.index;
1208
+ }
1209
+ const comparison = compareValue(
1210
+ sortValue(a.group, sort.key),
1211
+ sortValue(b.group, sort.key)
1212
+ );
1213
+ return (sort.direction === "asc" ? comparison : -comparison) || a.index - b.index;
1214
+ }).map(({ group }) => group);
1215
+ }
1216
+
1217
+ // src/SelectColumnEditor.tsx
1218
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1219
+ var FormulaEditor = lazy(() => import("./FormulaEditor-YCPICDIH.js"));
1220
+ var fieldExpression = (name) => `[${name.replace(/\]/g, "]]")}]`;
1221
+ var display = (v) => v === null ? "NULL" : typeof v === "string" ? v === "" ? '""' : v : JSON.stringify(v);
1222
+ function SelectColumnEditor({
1223
+ api,
1224
+ onClose
1225
+ }) {
1226
+ const titleId = useId(), dialog = useRef2(null);
1227
+ const closeRef = useRef2(onClose);
1228
+ closeRef.current = onClose;
1229
+ const [columns, setColumns] = useState3(
1230
+ () => api.select.visible.map((c) => ({ ...c }))
1231
+ );
1232
+ const [search, setSearch] = useState3(""), [active, setActive] = useState3(api.select.visible[0]?.field ?? "");
1233
+ const [editing, setEditing] = useState3(false), [id, setId] = useState3(""), [revision, setRevision] = useState3(null);
1234
+ const [label, setLabel] = useState3(""), [source, setSource] = useState3("");
1235
+ const [count, setCount] = useState3(1e3), [preview, setPreview] = useState3(null), [loading, setLoading] = useState3(false);
1236
+ const [error, setError] = useState3(null), [notice, setNotice] = useState3(""), [saving, setSaving] = useState3(false);
1237
+ const [filter, setFilter] = useState3("all"), [page, setPage] = useState3(0), [retry, setRetry] = useState3(0), [functionSearch, setFunctionSearch] = useState3("");
1238
+ const [previewSort, setPreviewSort] = useState3(null);
1239
+ const drag = useRef2(null);
1240
+ const [inspection, setInspection] = useState3(null);
1241
+ const catalogue = api.computed.catalogue.filter(isSelectable);
1242
+ const chosen = catalogue.find((f) => f.name === active);
1243
+ const expression = editing ? source : active ? fieldExpression(active) : "";
1244
+ const validation = useMemo2(() => {
1245
+ if (!expression)
1246
+ return { error: "Choose a column or create a computed column." };
1247
+ try {
1248
+ return {
1249
+ plan: api.computed.compile(expression, editing ? id : void 0)
1250
+ };
1251
+ } catch (e) {
1252
+ return { error: e instanceof Error ? e.message : String(e) };
1253
+ }
1254
+ }, [expression, editing, id, api.computed.compile]);
1255
+ useEffect3(() => {
1256
+ const previous = document.activeElement;
1257
+ dialog.current?.focus();
1258
+ const onKey = (e) => {
1259
+ if (e.key === "Escape") {
1260
+ if (document.querySelector(".cm-tooltip-autocomplete")) return;
1261
+ closeRef.current();
1262
+ }
1263
+ if (e.key === "Tab") {
1264
+ const focusable = Array.from(
1265
+ dialog.current?.querySelectorAll(
1266
+ 'button:not([disabled]), input:not([disabled]), select, textarea, [tabindex="0"], [contenteditable="true"]'
1267
+ ) ?? []
1268
+ ).filter((el) => el.getClientRects().length);
1269
+ const first = focusable[0], last = focusable[focusable.length - 1];
1270
+ if (e.shiftKey && (document.activeElement === first || document.activeElement === dialog.current)) {
1271
+ e.preventDefault();
1272
+ last?.focus();
1273
+ } else if (!e.shiftKey && document.activeElement === last) {
1274
+ e.preventDefault();
1275
+ first?.focus();
1276
+ }
1277
+ }
1278
+ };
1279
+ document.addEventListener("keydown", onKey);
1280
+ const old = document.body.style.overflow;
1281
+ document.body.style.overflow = "hidden";
1282
+ return () => {
1283
+ document.removeEventListener("keydown", onKey);
1284
+ document.body.style.overflow = old;
1285
+ previous?.focus();
1286
+ };
1287
+ }, []);
1288
+ useEffect3(() => {
1289
+ const ac = new AbortController();
1290
+ setPreview(null);
1291
+ setError(null);
1292
+ setPage(0);
1293
+ setInspection(null);
1294
+ if (validation.error || !Number.isInteger(count) || count < 1 || count > 1e4) {
1295
+ setLoading(false);
1296
+ return;
1297
+ }
1298
+ setLoading(true);
1299
+ const timer = setTimeout(() => {
1300
+ void api.computed.preview(
1301
+ expression,
1302
+ count,
1303
+ ac.signal,
1304
+ editing ? id : void 0,
1305
+ editing
1306
+ ).then((result) => {
1307
+ if (!ac.signal.aborted) setPreview(result);
1308
+ }).catch((e) => {
1309
+ if (!ac.signal.aborted)
1310
+ setError(e instanceof Error ? e.message : String(e));
1311
+ }).finally(() => {
1312
+ if (!ac.signal.aborted) setLoading(false);
1313
+ });
1314
+ }, 400);
1315
+ return () => {
1316
+ ac.abort();
1317
+ clearTimeout(timer);
1318
+ };
1319
+ }, [expression, count, editing, id, validation, api.computed.preview, retry]);
1320
+ const move = (field, index) => setColumns((cols) => {
1321
+ const next = cols.filter((c) => c.field !== field), item = cols.find((c) => c.field === field);
1322
+ if (item) next.splice(Math.max(0, Math.min(index, next.length)), 0, item);
1323
+ return next;
1324
+ });
1325
+ const toggle = (field) => setColumns(
1326
+ (cols) => cols.some((c) => c.field === field) ? cols.filter((c) => c.field !== field) : [...cols, { field }]
1327
+ );
1328
+ function browse(field) {
1329
+ setActive(field);
1330
+ setEditing(false);
1331
+ setNotice("");
1332
+ }
1333
+ function edit(defId) {
1334
+ const def = api.computed.definitions.find((d) => d.id === defId);
1335
+ setId(def?.id ?? defId ?? crypto.randomUUID());
1336
+ setRevision(def?.revision ?? null);
1337
+ setLabel(def?.label ?? defId ?? "");
1338
+ setSource(
1339
+ def?.expression.source ?? (defId ? "" : active ? fieldExpression(active) : "LEFT([field], 3)")
1340
+ );
1341
+ setEditing(true);
1342
+ setNotice("");
1343
+ }
1344
+ async function save() {
1345
+ setSaving(true);
1346
+ setError(null);
1347
+ try {
1348
+ const def = await api.computed.save(
1349
+ {
1350
+ id,
1351
+ label: label.trim(),
1352
+ expression: { language: "qt-expr", version: 1, source }
1353
+ },
1354
+ revision
1355
+ );
1356
+ setRevision(def.revision);
1357
+ const field = computedFieldName(def.id);
1358
+ setActive(field);
1359
+ setColumns(
1360
+ (cols) => cols.some((c) => c.field === field) ? cols : [...cols, { field }]
1361
+ );
1362
+ setNotice(
1363
+ "Definition saved. All queries referencing this ID now use this definition. Apply columns to update this query\u2019s layout."
1364
+ );
1365
+ } catch (e) {
1366
+ setError(e instanceof Error ? e.message : String(e));
1367
+ } finally {
1368
+ setSaving(false);
1369
+ }
1370
+ }
1371
+ const groups = useMemo2(
1372
+ () => orderPreviewGroups(
1373
+ (preview?.groups ?? []).filter(
1374
+ (g) => filter === "errors" ? !!g.result.error : filter === "nulls" ? !g.result.error && g.result.value === null : true
1375
+ ),
1376
+ previewSort
1377
+ ),
1378
+ [preview, filter, previewSort]
1379
+ );
1380
+ const shown = groups.slice(page * 50, (page + 1) * 50);
1381
+ const samePreviewSortKey = (a, b) => {
1382
+ if (a.kind !== b.kind) return false;
1383
+ if (a.kind === "input" && b.kind === "input")
1384
+ return a.index === b.index;
1385
+ return true;
1386
+ };
1387
+ const selectPreviewSort = (key, direction) => {
1388
+ setPreviewSort((current) => {
1389
+ const sameKey = !!current && samePreviewSortKey(current.key, key);
1390
+ return sameKey ? { key, direction: current.direction === "asc" ? "desc" : "asc" } : { key, direction };
1391
+ });
1392
+ setPage(0);
1393
+ };
1394
+ const previewSortDirection = (key) => previewSort && samePreviewSortKey(previewSort.key, key) ? previewSort.direction : void 0;
1395
+ const previewAriaSort = (key) => {
1396
+ const direction = previewSortDirection(key);
1397
+ return direction === "asc" ? "ascending" : direction === "desc" ? "descending" : "none";
1398
+ };
1399
+ const previewSortHeader = (label2, key, direction = "asc") => {
1400
+ const currentDirection = previewSortDirection(key);
1401
+ return /* @__PURE__ */ jsxs3(
1402
+ "button",
1403
+ {
1404
+ type: "button",
1405
+ className: "qt-preview-sort",
1406
+ onClick: () => selectPreviewSort(key, direction),
1407
+ title: `Sort by ${label2}`,
1408
+ children: [
1409
+ label2,
1410
+ currentDirection && (currentDirection === "asc" ? " \u2191" : " \u2193")
1411
+ ]
1412
+ }
1413
+ );
1414
+ };
1415
+ return /* @__PURE__ */ jsx3("div", { className: "qt-modal-backdrop qt-select-editor-backdrop", children: /* @__PURE__ */ jsxs3(
1416
+ "div",
1417
+ {
1418
+ className: "qt-select-editor",
1419
+ role: "dialog",
1420
+ "aria-modal": "true",
1421
+ "aria-labelledby": titleId,
1422
+ ref: dialog,
1423
+ tabIndex: -1,
1424
+ children: [
1425
+ /* @__PURE__ */ jsxs3("header", { className: "qt-select-editor-header", children: [
1426
+ /* @__PURE__ */ jsxs3("div", { children: [
1427
+ /* @__PURE__ */ jsx3("h2", { id: titleId, children: "Select columns" }),
1428
+ /* @__PURE__ */ jsx3("p", { children: "Arrange your view, explore values, and create reusable computed columns." })
1429
+ ] }),
1430
+ /* @__PURE__ */ jsx3(
1431
+ "button",
1432
+ {
1433
+ type: "button",
1434
+ className: "qt-btn",
1435
+ onClick: onClose,
1436
+ "aria-label": "Close column editor",
1437
+ children: "\u2715"
1438
+ }
1439
+ )
1440
+ ] }),
1441
+ /* @__PURE__ */ jsxs3("div", { className: "qt-select-editor-body", children: [
1442
+ /* @__PURE__ */ jsxs3("aside", { className: "qt-selected-panel", children: [
1443
+ /* @__PURE__ */ jsxs3("h3", { children: [
1444
+ "Selected ",
1445
+ /* @__PURE__ */ jsx3("span", { className: "qt-muted", children: columns.length })
1446
+ ] }),
1447
+ /* @__PURE__ */ jsx3("p", { className: "qt-muted", children: "Drag to reorder, or use the arrow buttons." }),
1448
+ /* @__PURE__ */ jsx3("ol", { className: "qt-editor-columns", children: columns.map((c, i) => {
1449
+ const f = catalogue.find((f2) => f2.name === c.field);
1450
+ return /* @__PURE__ */ jsxs3(
1451
+ "li",
1452
+ {
1453
+ draggable: true,
1454
+ onDragStart: (e) => {
1455
+ drag.current = c.field;
1456
+ e.dataTransfer.setData("text/plain", c.field);
1457
+ },
1458
+ onDragOver: (e) => e.preventDefault(),
1459
+ onDrop: (e) => {
1460
+ e.preventDefault();
1461
+ if (drag.current) move(drag.current, i);
1462
+ drag.current = null;
1463
+ },
1464
+ onDragEnd: () => {
1465
+ drag.current = null;
1466
+ },
1467
+ children: [
1468
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\u283F" }),
1469
+ /* @__PURE__ */ jsx3(
1470
+ "button",
1471
+ {
1472
+ type: "button",
1473
+ className: "qt-column-name",
1474
+ onClick: () => browse(c.field),
1475
+ children: f?.label ?? c.field
1476
+ }
1477
+ ),
1478
+ /* @__PURE__ */ jsx3(
1479
+ "button",
1480
+ {
1481
+ type: "button",
1482
+ className: "qt-icon-btn",
1483
+ disabled: i === 0,
1484
+ onClick: () => move(c.field, i - 1),
1485
+ "aria-label": `Move ${f?.label ?? c.field} up`,
1486
+ children: "\u2191"
1487
+ }
1488
+ ),
1489
+ /* @__PURE__ */ jsx3(
1490
+ "button",
1491
+ {
1492
+ type: "button",
1493
+ className: "qt-icon-btn",
1494
+ disabled: i === columns.length - 1,
1495
+ onClick: () => move(c.field, i + 1),
1496
+ "aria-label": `Move ${f?.label ?? c.field} down`,
1497
+ children: "\u2193"
1498
+ }
1499
+ ),
1500
+ /* @__PURE__ */ jsx3(
1501
+ "button",
1502
+ {
1503
+ type: "button",
1504
+ className: "qt-icon-btn",
1505
+ onClick: () => toggle(c.field),
1506
+ "aria-label": `Remove ${f?.label ?? c.field}`,
1507
+ children: "\xD7"
1508
+ }
1509
+ )
1510
+ ]
1511
+ },
1512
+ c.field
1513
+ );
1514
+ }) }),
1515
+ /* @__PURE__ */ jsx3(
1516
+ "button",
1517
+ {
1518
+ type: "button",
1519
+ className: "qt-link-btn",
1520
+ onClick: () => setColumns(
1521
+ api.defaults.select.length ? api.defaults.select : catalogue.filter((f) => f.select?.default).map((f) => ({ field: f.name }))
1522
+ ),
1523
+ children: "Use default columns"
1524
+ }
1525
+ )
1526
+ ] }),
1527
+ /* @__PURE__ */ jsxs3("aside", { className: "qt-catalogue-panel", children: [
1528
+ /* @__PURE__ */ jsx3("h3", { children: "Column catalogue" }),
1529
+ /* @__PURE__ */ jsx3(
1530
+ "input",
1531
+ {
1532
+ className: "qt-input",
1533
+ "aria-label": "Search columns",
1534
+ placeholder: "Search name or type\u2026",
1535
+ value: search,
1536
+ onChange: (e) => setSearch(e.target.value)
1537
+ }
1538
+ ),
1539
+ /* @__PURE__ */ jsx3(
1540
+ "button",
1541
+ {
1542
+ type: "button",
1543
+ className: "qt-btn qt-create-computed",
1544
+ onClick: () => edit(),
1545
+ children: "\uFF0B Computed column"
1546
+ }
1547
+ ),
1548
+ api.computed.loading && /* @__PURE__ */ jsx3("p", { role: "status", children: "Loading definitions\u2026" }),
1549
+ api.computed.error && /* @__PURE__ */ jsx3("p", { role: "alert", children: api.computed.error }),
1550
+ /* @__PURE__ */ jsx3("div", { className: "qt-catalogue-list", children: catalogue.filter(
1551
+ (f) => `${f.label} ${f.name} ${f.type} ${f.group ?? ""}`.toLowerCase().includes(search.toLowerCase())
1552
+ ).map((f) => /* @__PURE__ */ jsxs3(
1553
+ "div",
1554
+ {
1555
+ className: `qt-catalogue-item ${active === f.name && !editing ? "is-active" : ""}`,
1556
+ children: [
1557
+ /* @__PURE__ */ jsx3(
1558
+ "input",
1559
+ {
1560
+ type: "checkbox",
1561
+ "aria-label": `Include ${f.label}`,
1562
+ checked: columns.some((c) => c.field === f.name),
1563
+ onChange: () => toggle(f.name)
1564
+ }
1565
+ ),
1566
+ /* @__PURE__ */ jsxs3("button", { type: "button", onClick: () => browse(f.name), children: [
1567
+ /* @__PURE__ */ jsx3("strong", { children: f.label }),
1568
+ /* @__PURE__ */ jsxs3("small", { children: [
1569
+ isComputedField(f.name) ? "\u0192 \xB7 " : "",
1570
+ f.type,
1571
+ " \xB7 ",
1572
+ f.group ?? f.name
1573
+ ] })
1574
+ ] })
1575
+ ]
1576
+ },
1577
+ f.name
1578
+ )) }),
1579
+ /* @__PURE__ */ jsx3(
1580
+ "button",
1581
+ {
1582
+ type: "button",
1583
+ className: "qt-link-btn",
1584
+ onClick: () => void api.computed.reload(),
1585
+ children: "Reload shared definitions"
1586
+ }
1587
+ )
1588
+ ] }),
1589
+ /* @__PURE__ */ jsxs3("section", { className: "qt-column-workbench", children: [
1590
+ editing ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
1591
+ /* @__PURE__ */ jsxs3("div", { className: "qt-editor-title", children: [
1592
+ /* @__PURE__ */ jsx3("h3", { children: revision ? "Edit shared computed column" : "New computed column" }),
1593
+ /* @__PURE__ */ jsx3("span", { className: "qt-type-badge", children: validation.plan?.type ?? "Check formula" })
1594
+ ] }),
1595
+ /* @__PURE__ */ jsxs3("label", { className: "qt-editor-label", children: [
1596
+ "Column name",
1597
+ /* @__PURE__ */ jsx3(
1598
+ "input",
1599
+ {
1600
+ className: "qt-input",
1601
+ value: label,
1602
+ maxLength: 200,
1603
+ onChange: (e) => setLabel(e.target.value),
1604
+ placeholder: "e.g. Email domain"
1605
+ }
1606
+ )
1607
+ ] }),
1608
+ /* @__PURE__ */ jsx3(
1609
+ Suspense,
1610
+ {
1611
+ fallback: /* @__PURE__ */ jsx3(
1612
+ "textarea",
1613
+ {
1614
+ "aria-label": "Computed column formula",
1615
+ value: source,
1616
+ onChange: (e) => setSource(e.target.value)
1617
+ }
1618
+ ),
1619
+ children: /* @__PURE__ */ jsx3(
1620
+ FormulaEditor,
1621
+ {
1622
+ value: source,
1623
+ onChange: setSource,
1624
+ fields: api.computed.catalogue.filter(
1625
+ (f) => f.name !== computedFieldName(id)
1626
+ ),
1627
+ compile: (value) => api.computed.compile(value, id)
1628
+ }
1629
+ )
1630
+ }
1631
+ ),
1632
+ /* @__PURE__ */ jsxs3("details", { className: "qt-function-library", children: [
1633
+ /* @__PURE__ */ jsx3("summary", { children: "Functions & examples" }),
1634
+ /* @__PURE__ */ jsx3(
1635
+ "input",
1636
+ {
1637
+ className: "qt-input",
1638
+ "aria-label": "Search functions",
1639
+ placeholder: "Search functions\u2026",
1640
+ value: functionSearch,
1641
+ onChange: (e) => setFunctionSearch(e.target.value)
1642
+ }
1643
+ ),
1644
+ /* @__PURE__ */ jsx3("div", { className: "qt-function-list", children: FORMULA_FUNCTIONS.filter(
1645
+ (f) => `${f.name} ${f.description}`.toLowerCase().includes(functionSearch.toLowerCase())
1646
+ ).map((f) => /* @__PURE__ */ jsxs3(
1647
+ "button",
1648
+ {
1649
+ type: "button",
1650
+ title: f.description,
1651
+ onClick: () => setSource(`${f.name}(${source})`),
1652
+ children: [
1653
+ /* @__PURE__ */ jsx3("code", { children: f.signature }),
1654
+ /* @__PURE__ */ jsx3("small", { children: f.description })
1655
+ ]
1656
+ },
1657
+ f.name
1658
+ )) }),
1659
+ /* @__PURE__ */ jsxs3("p", { children: [
1660
+ "Examples: ",
1661
+ /* @__PURE__ */ jsx3("code", { children: "LEFT([name], 3)" }),
1662
+ " \xB7",
1663
+ " ",
1664
+ /* @__PURE__ */ jsx3("code", { children: 'IF([amount] > 100, "High", "Low")' }),
1665
+ " \xB7",
1666
+ " ",
1667
+ /* @__PURE__ */ jsx3("code", { children: 'REGEX_EXTRACT([email], "@(.+)$", 1)' })
1668
+ ] })
1669
+ ] }),
1670
+ /* @__PURE__ */ jsxs3("div", { className: "qt-definition-actions", children: [
1671
+ /* @__PURE__ */ jsx3(
1672
+ "button",
1673
+ {
1674
+ type: "button",
1675
+ className: "qt-btn",
1676
+ disabled: saving || !!validation.error || !label.trim(),
1677
+ onClick: () => void save(),
1678
+ children: saving ? "Saving\u2026" : revision ? "Save shared definition" : "Save definition & add column"
1679
+ }
1680
+ ),
1681
+ revision && /* @__PURE__ */ jsx3(
1682
+ "button",
1683
+ {
1684
+ type: "button",
1685
+ className: "qt-link-btn",
1686
+ onClick: () => edit(id),
1687
+ children: "Reload editor from catalogue"
1688
+ }
1689
+ )
1690
+ ] }),
1691
+ /* @__PURE__ */ jsx3("p", { className: "qt-muted", children: "Saving changes this definition across all queries. Cancel below only discards column layout changes." })
1692
+ ] }) : /* @__PURE__ */ jsxs3("div", { className: "qt-editor-title", children: [
1693
+ /* @__PURE__ */ jsxs3("div", { children: [
1694
+ /* @__PURE__ */ jsx3("h3", { children: chosen?.label ?? "Explore a column" }),
1695
+ /* @__PURE__ */ jsxs3("p", { className: "qt-muted", children: [
1696
+ chosen?.name,
1697
+ " ",
1698
+ chosen ? `\xB7 ${chosen.type}` : ""
1699
+ ] })
1700
+ ] }),
1701
+ chosen && isComputedField(chosen.name) && /* @__PURE__ */ jsxs3(Fragment3, { children: [
1702
+ /* @__PURE__ */ jsx3(
1703
+ "button",
1704
+ {
1705
+ type: "button",
1706
+ className: "qt-btn",
1707
+ onClick: () => edit(chosen.name.slice(10)),
1708
+ children: "Edit definition"
1709
+ }
1710
+ ),
1711
+ /* @__PURE__ */ jsx3(
1712
+ "button",
1713
+ {
1714
+ type: "button",
1715
+ className: "qt-btn",
1716
+ onClick: () => {
1717
+ const def = api.computed.definitions.find(
1718
+ (d) => computedFieldName(d.id) === chosen.name
1719
+ );
1720
+ edit();
1721
+ if (def) {
1722
+ setLabel(`${def.label} copy`);
1723
+ setSource(def.expression.source);
1724
+ }
1725
+ },
1726
+ children: "Duplicate"
1727
+ }
1728
+ )
1729
+ ] })
1730
+ ] }),
1731
+ /* @__PURE__ */ jsxs3("div", { className: "qt-preview-controls", children: [
1732
+ /* @__PURE__ */ jsxs3("label", { children: [
1733
+ "Rows to process",
1734
+ " ",
1735
+ /* @__PURE__ */ jsx3(
1736
+ "input",
1737
+ {
1738
+ className: "qt-input",
1739
+ type: "number",
1740
+ min: 1,
1741
+ max: 1e4,
1742
+ step: 1,
1743
+ value: Number.isNaN(count) ? "" : count,
1744
+ onChange: (e) => setCount(e.target.valueAsNumber)
1745
+ }
1746
+ )
1747
+ ] }),
1748
+ /* @__PURE__ */ jsx3(
1749
+ "button",
1750
+ {
1751
+ type: "button",
1752
+ className: "qt-btn",
1753
+ onClick: () => setRetry((n) => n + 1),
1754
+ children: "Refresh preview"
1755
+ }
1756
+ ),
1757
+ /* @__PURE__ */ jsxs3("label", { children: [
1758
+ "Show",
1759
+ " ",
1760
+ /* @__PURE__ */ jsxs3(
1761
+ "select",
1762
+ {
1763
+ value: filter,
1764
+ onChange: (e) => {
1765
+ setFilter(e.target.value);
1766
+ setPage(0);
1767
+ },
1768
+ children: [
1769
+ /* @__PURE__ */ jsx3("option", { value: "all", children: "All results" }),
1770
+ /* @__PURE__ */ jsx3("option", { value: "nulls", children: "Null results" }),
1771
+ /* @__PURE__ */ jsx3("option", { value: "errors", children: "Errors" })
1772
+ ]
1773
+ }
1774
+ )
1775
+ ] })
1776
+ ] }),
1777
+ /* @__PURE__ */ jsx3("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." }),
1778
+ (!Number.isInteger(count) || count < 1 || count > 1e4) && /* @__PURE__ */ jsx3("p", { role: "alert", children: "Choose a whole number between 1 and 10,000." }),
1779
+ validation.error && /* @__PURE__ */ jsx3("p", { className: "qt-formula-error", role: "alert", children: validation.error }),
1780
+ error && /* @__PURE__ */ jsx3("p", { className: "qt-formula-error", role: "alert", children: error }),
1781
+ notice && /* @__PURE__ */ jsx3("p", { className: "qt-editor-notice", role: "status", children: notice }),
1782
+ loading && /* @__PURE__ */ jsx3("p", { role: "status", children: "Processing preview\u2026" }),
1783
+ preview && /* @__PURE__ */ jsxs3(Fragment3, { children: [
1784
+ /* @__PURE__ */ jsxs3("p", { className: "qt-preview-summary", role: "status", children: [
1785
+ /* @__PURE__ */ jsx3("strong", { children: preview.processed.toLocaleString() }),
1786
+ " of",
1787
+ " ",
1788
+ preview.total.toLocaleString(),
1789
+ " matching rows \xB7",
1790
+ " ",
1791
+ /* @__PURE__ */ jsx3("strong", { children: preview.groups.length.toLocaleString() }),
1792
+ " ",
1793
+ "distinct combinations \xB7 ",
1794
+ preview.nulls.toLocaleString(),
1795
+ " null \xB7 ",
1796
+ preview.errors.toLocaleString(),
1797
+ " errors",
1798
+ preview.processed === preview.total ? " \xB7 complete filtered set" : " \xB7 sample"
1799
+ ] }),
1800
+ /* @__PURE__ */ jsx3("div", { className: "qt-preview-table-wrap", children: /* @__PURE__ */ jsxs3("table", { className: "qt-preview-table", children: [
1801
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs3("tr", { children: [
1802
+ editing && preview.dependencies.map((name, index) => /* @__PURE__ */ jsx3(
1803
+ "th",
1804
+ {
1805
+ "aria-sort": previewAriaSort({
1806
+ kind: "input",
1807
+ index
1808
+ }),
1809
+ children: previewSortHeader(
1810
+ api.computed.catalogue.find(
1811
+ (f) => f.name === name
1812
+ )?.label ?? name,
1813
+ { kind: "input", index }
1814
+ )
1815
+ },
1816
+ name
1817
+ )),
1818
+ /* @__PURE__ */ jsx3("th", { "aria-sort": previewAriaSort({ kind: "result" }), children: previewSortHeader(editing ? "Result" : "Value", {
1819
+ kind: "result"
1820
+ }) }),
1821
+ /* @__PURE__ */ jsx3("th", { "aria-sort": previewAriaSort({ kind: "count" }), children: previewSortHeader("Count", { kind: "count" }, "desc") }),
1822
+ /* @__PURE__ */ jsx3("th", { "aria-sort": previewAriaSort({ kind: "percentage" }), children: previewSortHeader("%", { kind: "percentage" }, "desc") })
1823
+ ] }) }),
1824
+ /* @__PURE__ */ jsx3("tbody", { children: shown.map((group, i) => /* @__PURE__ */ jsxs3("tr", { children: [
1825
+ editing && group.inputs.map((value, j) => /* @__PURE__ */ jsx3("td", { title: display(value), children: display(value) }, j)),
1826
+ /* @__PURE__ */ jsxs3(
1827
+ "td",
1828
+ {
1829
+ className: group.result.error ? "qt-formula-error" : "",
1830
+ title: group.result.error ?? display(group.result.value),
1831
+ children: [
1832
+ group.result.error ?? display(group.result.value),
1833
+ group.result.regex && /* @__PURE__ */ jsx3(
1834
+ "button",
1835
+ {
1836
+ type: "button",
1837
+ className: "qt-link-btn",
1838
+ onClick: () => setInspection(group.result.regex),
1839
+ children: "Inspect match"
1840
+ }
1841
+ )
1842
+ ]
1843
+ }
1844
+ ),
1845
+ /* @__PURE__ */ jsx3("td", { children: group.count.toLocaleString() }),
1846
+ /* @__PURE__ */ jsxs3("td", { children: [
1847
+ (100 * group.count / preview.processed).toFixed(
1848
+ 1
1849
+ ),
1850
+ "%"
1851
+ ] })
1852
+ ] }, i)) })
1853
+ ] }) }),
1854
+ inspection && /* @__PURE__ */ jsxs3("div", { className: "qt-regex-inspector", children: [
1855
+ /* @__PURE__ */ jsx3("h4", { children: "Regex match" }),
1856
+ /* @__PURE__ */ jsxs3("pre", { children: [
1857
+ inspection.input.slice(0, inspection.start),
1858
+ /* @__PURE__ */ jsx3("mark", { children: inspection.input.slice(
1859
+ inspection.start,
1860
+ inspection.end
1861
+ ) }),
1862
+ inspection.input.slice(inspection.end)
1863
+ ] }),
1864
+ /* @__PURE__ */ jsx3("dl", { children: inspection.groups.map((value, i) => /* @__PURE__ */ jsxs3("div", { children: [
1865
+ /* @__PURE__ */ jsxs3("dt", { children: [
1866
+ "Group ",
1867
+ i,
1868
+ i === 0 ? " (whole match)" : ""
1869
+ ] }),
1870
+ /* @__PURE__ */ jsx3("dd", { children: display(value) })
1871
+ ] }, i)) })
1872
+ ] }),
1873
+ !groups.length && /* @__PURE__ */ jsx3("p", { children: "No matching results in this sample." }),
1874
+ groups.length > 50 && /* @__PURE__ */ jsxs3("div", { className: "qt-preview-pages", children: [
1875
+ /* @__PURE__ */ jsx3(
1876
+ "button",
1877
+ {
1878
+ type: "button",
1879
+ className: "qt-btn",
1880
+ disabled: page === 0,
1881
+ onClick: () => setPage((n) => n - 1),
1882
+ children: "Previous"
1883
+ }
1884
+ ),
1885
+ /* @__PURE__ */ jsxs3("span", { children: [
1886
+ "Combinations ",
1887
+ page * 50 + 1,
1888
+ "\u2013",
1889
+ Math.min((page + 1) * 50, groups.length),
1890
+ " of",
1891
+ " ",
1892
+ groups.length
1893
+ ] }),
1894
+ /* @__PURE__ */ jsx3(
1895
+ "button",
1896
+ {
1897
+ type: "button",
1898
+ className: "qt-btn",
1899
+ disabled: (page + 1) * 50 >= groups.length,
1900
+ onClick: () => setPage((n) => n + 1),
1901
+ children: "Next"
1902
+ }
1903
+ )
1904
+ ] })
1905
+ ] })
1906
+ ] })
1907
+ ] }),
1908
+ /* @__PURE__ */ jsxs3("footer", { className: "qt-select-editor-footer", children: [
1909
+ /* @__PURE__ */ jsx3("span", { className: "qt-muted", children: "Computed columns are display-only. Select at least one column." }),
1910
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "qt-btn", onClick: onClose, children: "Cancel layout changes" }),
1911
+ /* @__PURE__ */ jsx3(
1912
+ "button",
1913
+ {
1914
+ type: "button",
1915
+ className: "qt-btn qt-primary",
1916
+ disabled: !columns.length || columns.length > 200,
1917
+ onClick: () => {
1918
+ api.setQuery((q) => ({ ...q, select: columns }));
1919
+ onClose();
1920
+ },
1921
+ children: "Apply columns"
1922
+ }
1923
+ )
1924
+ ] })
1925
+ ]
1926
+ }
1927
+ ) });
1928
+ }
1929
+
1117
1930
  // src/QueryBuilder.tsx
1118
- import { useEffect as useEffect4, useId, useMemo as useMemo3, useRef as useRef2, useState as useState4 } from "react";
1931
+ import { Fragment as Fragment5, useEffect as useEffect5, useId as useId2, useMemo as useMemo4, useRef as useRef3, useState as useState5 } from "react";
1119
1932
  import {
1120
1933
  NULLARY_OPS as NULLARY_OPS2,
1121
1934
  aggOpNeedsField,
@@ -1124,16 +1937,19 @@ import {
1124
1937
  filterValues as filterValuesFor,
1125
1938
  isGroupable,
1126
1939
  isMeasurable,
1940
+ isOrGroup,
1941
+ opPairsForField as opPairsForField2,
1942
+ predicatesOf,
1127
1943
  queriesEqual,
1128
- isFilterable,
1129
- isSelectable,
1944
+ isFilterable as isFilterable2,
1945
+ isSelectable as isSelectable2,
1130
1946
  isSortable as isSortable2,
1131
1947
  opsForField as opsForField2
1132
1948
  } from "@pythia-software/query-table-core";
1133
1949
 
1134
1950
  // 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";
1951
+ import { useMemo as useMemo3, useState as useState4 } from "react";
1952
+ import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1137
1953
  var UNGROUPED = "other";
1138
1954
  function FieldPicker({
1139
1955
  fields,
@@ -1143,10 +1959,10 @@ function FieldPicker({
1143
1959
  onPick,
1144
1960
  onClose
1145
1961
  }) {
1146
- const [search, setSearch] = useState3("");
1962
+ const [search, setSearch] = useState4("");
1147
1963
  const needle = search.trim().toLowerCase();
1148
- const excludedSet = useMemo2(() => new Set(excluded ?? []), [excluded]);
1149
- const matched = useMemo2(
1964
+ const excludedSet = useMemo3(() => new Set(excluded ?? []), [excluded]);
1965
+ const matched = useMemo3(
1150
1966
  () => fields.filter((f) => {
1151
1967
  if (excludedSet.has(f.name)) return false;
1152
1968
  if (!needle) return true;
@@ -1154,10 +1970,10 @@ function FieldPicker({
1154
1970
  }),
1155
1971
  [fields, excludedSet, needle]
1156
1972
  );
1157
- const grouped = useMemo2(() => groupByGroup(matched), [matched]);
1973
+ const grouped = useMemo3(() => groupByGroup(matched), [matched]);
1158
1974
  const firstEnabled = matched.find((f) => isEnabled(f, stats, gateOnDistinct));
1159
- return /* @__PURE__ */ jsxs3("div", { className: "qt-picker", children: [
1160
- /* @__PURE__ */ jsx3(
1975
+ return /* @__PURE__ */ jsxs4("div", { className: "qt-picker", children: [
1976
+ /* @__PURE__ */ jsx4(
1161
1977
  "input",
1162
1978
  {
1163
1979
  autoFocus: true,
@@ -1176,13 +1992,13 @@ function FieldPicker({
1176
1992
  onBlur: () => setTimeout(() => onClose?.(), 150)
1177
1993
  }
1178
1994
  ),
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 }),
1995
+ /* @__PURE__ */ jsx4("div", { className: "qt-picker-list", children: matched.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "qt-picker-empty", children: "no matches" }) : grouped.map(([group, fs]) => /* @__PURE__ */ jsxs4("div", { className: "qt-picker-group", children: [
1996
+ /* @__PURE__ */ jsx4("div", { className: "qt-picker-group-label", children: group }),
1181
1997
  fs.map((f) => {
1182
1998
  const enabled = isEnabled(f, stats, gateOnDistinct);
1183
1999
  const s = stats?.[f.name];
1184
2000
  const aliasHit = aliasMatch(f, needle);
1185
- return /* @__PURE__ */ jsxs3(
2001
+ return /* @__PURE__ */ jsxs4(
1186
2002
  "button",
1187
2003
  {
1188
2004
  type: "button",
@@ -1196,20 +2012,20 @@ function FieldPicker({
1196
2012
  setSearch("");
1197
2013
  },
1198
2014
  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 })
2015
+ /* @__PURE__ */ jsxs4("span", { className: "qt-picker-row", children: [
2016
+ /* @__PURE__ */ jsx4("span", { className: "qt-picker-label", children: f.label }),
2017
+ /* @__PURE__ */ jsx4("span", { className: "qt-picker-name", children: f.name })
1202
2018
  ] }),
1203
- aliasHit && /* @__PURE__ */ jsxs3("span", { className: "qt-picker-alias", children: [
2019
+ aliasHit && /* @__PURE__ */ jsxs4("span", { className: "qt-picker-alias", children: [
1204
2020
  "matches: ",
1205
2021
  aliasHit
1206
2022
  ] }),
1207
- s && /* @__PURE__ */ jsxs3("span", { className: "qt-picker-stats", children: [
1208
- s.distinct != null && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2023
+ s && /* @__PURE__ */ jsxs4("span", { className: "qt-picker-stats", children: [
2024
+ s.distinct != null && /* @__PURE__ */ jsxs4(Fragment4, { children: [
1209
2025
  s.distinct,
1210
2026
  " distinct"
1211
2027
  ] }),
1212
- s.min != null && s.max != null && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2028
+ s.min != null && s.max != null && /* @__PURE__ */ jsxs4(Fragment4, { children: [
1213
2029
  " ",
1214
2030
  "\xB7 ",
1215
2031
  fmtStat(s.min),
@@ -1263,17 +2079,17 @@ function fmtStat(v) {
1263
2079
  }
1264
2080
 
1265
2081
  // src/SavedQueriesModal.tsx
1266
- import { useEffect as useEffect3 } from "react";
1267
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2082
+ import { useEffect as useEffect4 } from "react";
2083
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1268
2084
  function SavedQueriesModal({ saved, onClose, onLoad }) {
1269
- useEffect3(() => {
2085
+ useEffect4(() => {
1270
2086
  function onKey(e) {
1271
2087
  if (e.key === "Escape") onClose();
1272
2088
  }
1273
2089
  window.addEventListener("keydown", onKey);
1274
2090
  return () => window.removeEventListener("keydown", onKey);
1275
2091
  }, [onClose]);
1276
- return /* @__PURE__ */ jsx4("div", { className: "qt-modal-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs4(
2092
+ return /* @__PURE__ */ jsx5("div", { className: "qt-modal-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs5(
1277
2093
  "div",
1278
2094
  {
1279
2095
  className: "qt-modal",
@@ -1282,12 +2098,12 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1282
2098
  "aria-modal": "true",
1283
2099
  "aria-label": "Saved queries",
1284
2100
  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) => {
2101
+ /* @__PURE__ */ jsx5("h3", { className: "qt-modal-title", children: "Saved queries" }),
2102
+ saved.loading ? /* @__PURE__ */ jsx5("p", { className: "qt-muted", children: "Loading\u2026" }) : saved.items.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "qt-muted", children: "None saved yet. Save the current query from the toolbar." }) : /* @__PURE__ */ jsx5("ul", { className: "qt-saved-list", children: saved.items.map((s) => {
1287
2103
  const filters = s.query.where.length;
1288
2104
  const isDefault = saved.defaultId === s.id;
1289
- return /* @__PURE__ */ jsxs4("li", { className: "qt-saved-item", children: [
1290
- /* @__PURE__ */ jsxs4(
2105
+ return /* @__PURE__ */ jsxs5("li", { className: "qt-saved-item", children: [
2106
+ /* @__PURE__ */ jsxs5(
1291
2107
  "button",
1292
2108
  {
1293
2109
  type: "button",
@@ -1298,8 +2114,8 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1298
2114
  onClose();
1299
2115
  },
1300
2116
  children: [
1301
- /* @__PURE__ */ jsx4("span", { className: "qt-saved-name", children: s.name }),
1302
- /* @__PURE__ */ jsxs4("span", { className: "qt-saved-meta qt-muted", children: [
2117
+ /* @__PURE__ */ jsx5("span", { className: "qt-saved-name", children: s.name }),
2118
+ /* @__PURE__ */ jsxs5("span", { className: "qt-saved-meta qt-muted", children: [
1303
2119
  isDefault ? "Default view \xB7 " : "",
1304
2120
  new Date(s.savedAt).toLocaleString(),
1305
2121
  " \xB7 ",
@@ -1310,7 +2126,7 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1310
2126
  ]
1311
2127
  }
1312
2128
  ),
1313
- isDefault ? /* @__PURE__ */ jsx4(
2129
+ isDefault ? /* @__PURE__ */ jsx5(
1314
2130
  "button",
1315
2131
  {
1316
2132
  type: "button",
@@ -1319,7 +2135,7 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1319
2135
  onClick: () => void saved.clearDefault(),
1320
2136
  children: "default"
1321
2137
  }
1322
- ) : /* @__PURE__ */ jsx4(
2138
+ ) : /* @__PURE__ */ jsx5(
1323
2139
  "button",
1324
2140
  {
1325
2141
  type: "button",
@@ -1329,7 +2145,7 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1329
2145
  children: "make default"
1330
2146
  }
1331
2147
  ),
1332
- /* @__PURE__ */ jsx4(
2148
+ /* @__PURE__ */ jsx5(
1333
2149
  "button",
1334
2150
  {
1335
2151
  type: "button",
@@ -1341,14 +2157,14 @@ function SavedQueriesModal({ saved, onClose, onLoad }) {
1341
2157
  )
1342
2158
  ] }, s.id);
1343
2159
  }) }),
1344
- /* @__PURE__ */ jsx4("div", { className: "qt-modal-footer", children: /* @__PURE__ */ jsx4("button", { type: "button", className: "qt-btn", onClick: onClose, children: "close" }) })
2160
+ /* @__PURE__ */ jsx5("div", { className: "qt-modal-footer", children: /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-btn", onClick: onClose, children: "close" }) })
1345
2161
  ]
1346
2162
  }
1347
2163
  ) });
1348
2164
  }
1349
2165
 
1350
2166
  // src/QueryBuilder.tsx
1351
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2167
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1352
2168
  var cx3 = (...parts) => parts.filter((p) => Boolean(p)).join(" ");
1353
2169
  var MAX_AUTO_REFRESH_POLLS = 1e3;
1354
2170
  var DEFAULT_AUTO_REFRESH_FREQUENCY_MS = 2e4;
@@ -1392,8 +2208,8 @@ function formatStatusTime(ms) {
1392
2208
  return new Date(ms).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", second: "2-digit" });
1393
2209
  }
1394
2210
  function useFieldHasNull(api, fieldName) {
1395
- const [hasNull, setHasNull] = useState4(void 0);
1396
- useEffect4(() => {
2211
+ const [hasNull, setHasNull] = useState5(void 0);
2212
+ useEffect5(() => {
1397
2213
  if (!fieldName) return;
1398
2214
  let cancelled = false;
1399
2215
  void api.filterValues(fieldName, "").then((r) => {
@@ -1409,17 +2225,27 @@ function useFieldHasNull(api, fieldName) {
1409
2225
  }, [api, fieldName]);
1410
2226
  return hasNull;
1411
2227
  }
1412
- function whereClauseAsText(clause, byName) {
2228
+ var CONVERGED_NEGATIVE_OPS = /* @__PURE__ */ new Set(["!=", "is_not_null", "not_matches_regex"]);
2229
+ function positiveOpsForField(field) {
2230
+ return opsForField2(field).filter((op) => !CONVERGED_NEGATIVE_OPS.has(op));
2231
+ }
2232
+ function predicateAsText(clause, byName) {
1413
2233
  const field = byName.get(clause.field)?.label ?? clause.field;
1414
2234
  const op = clause.op.replace(/_/g, " ");
1415
- if (NULLARY_OPS2.has(clause.op)) return `${field} ${op}`;
2235
+ const not = clause.negated ? "not " : "";
2236
+ if (NULLARY_OPS2.has(clause.op)) return `${field} ${not}${op}`;
1416
2237
  const value = clause.value === "" ? "''" : clause.value;
1417
- return `${field} ${op} ${value}`;
2238
+ return `${field} ${not}${op} ${value}`;
2239
+ }
2240
+ function whereTermAsText(term, byName) {
2241
+ if (isOrGroup(term)) return `(${term.any.map((c) => predicateAsText(c, byName)).join(" or ")})`;
2242
+ return predicateAsText(term, byName);
1418
2243
  }
1419
2244
  function orderByAsText(term, byName) {
1420
2245
  const field = byName.get(term.field)?.label ?? term.field;
2246
+ const expression = term.extract ? `regex_extract(${field}, /${term.extract.regex}/)` : field;
1421
2247
  const nulls = term.nulls ? ` nulls ${term.nulls}` : "";
1422
- return `${field} ${term.dir}${nulls}`;
2248
+ return `${expression} ${term.dir}${nulls}`;
1423
2249
  }
1424
2250
  function chipFieldWidthChars(text, fallback = 5, max = 30) {
1425
2251
  return Math.min(max, Math.max(fallback, (text || "").length + 1));
@@ -1432,12 +2258,12 @@ function chipFieldWidthForSelect(text, fallback = 3, max = 30) {
1432
2258
  return chipFieldWidthChars(text, fallback, max) + 2;
1433
2259
  }
1434
2260
  function buildCollapsedSummary(where, orderBy, byName) {
1435
- const whereText = where.length === 0 ? "all rows" : where.map((c) => whereClauseAsText(c, byName)).join(" and ");
2261
+ const whereText = whereClauseSummaryText(where, byName);
1436
2262
  const orderText = orderBy.length === 0 ? "(default)" : orderBy.map((term) => orderByAsText(term, byName)).join(", ");
1437
2263
  return `WHERE ${whereText} ORDER BY ${orderText}`;
1438
2264
  }
1439
2265
  function whereClauseSummaryText(where, byName) {
1440
- return where.length === 0 ? "all rows" : where.map((c) => whereClauseAsText(c, byName)).join(" and ");
2266
+ return where.length === 0 ? "all rows" : where.map((t) => whereTermAsText(t, byName)).join(" and ");
1441
2267
  }
1442
2268
  function orderBySummaryText(orderBy, byName) {
1443
2269
  return orderBy.length === 0 ? "(default)" : orderBy.map((term) => orderByAsText(term, byName)).join(", ");
@@ -1453,15 +2279,15 @@ function QueryBuilder({
1453
2279
  defaultCollapsed = false
1454
2280
  }) {
1455
2281
  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);
2282
+ const [showSaved, setShowSaved] = useState5(false);
2283
+ const [shareCopied, setShareCopied] = useState5(false);
2284
+ const [showAutoRefresh, setShowAutoRefresh] = useState5(false);
2285
+ const [autoRefreshFrequencyMs, setAutoRefreshFrequencyMs] = useState5(DEFAULT_AUTO_REFRESH_FREQUENCY_MS);
2286
+ const [autoRefreshTurnOffAfterMs, setAutoRefreshTurnOffAfterMs] = useState5(DEFAULT_AUTO_REFRESH_TURN_OFF_AFTER_MS);
2287
+ const [editingSavedId, setEditingSavedId] = useState5(null);
2288
+ const [editingSavedName, setEditingSavedName] = useState5("");
2289
+ const [lastSavedId, setLastSavedId] = useState5(null);
2290
+ const [collapsedFallback, setCollapsedFallback] = useState5(defaultCollapsed);
1465
2291
  const collapsedControlled = typeof collapsed !== "undefined";
1466
2292
  const isCollapsed = collapsedControlled ? collapsed : collapsedFallback;
1467
2293
  const setCollapsed = (next) => {
@@ -1471,31 +2297,31 @@ function QueryBuilder({
1471
2297
  }
1472
2298
  setCollapsedFallback(next);
1473
2299
  };
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(
2300
+ const [lastUpdatedAt, setLastUpdatedAt] = useState5(null);
2301
+ const [lastFailureAt, setLastFailureAt] = useState5(null);
2302
+ const byName = useMemo4(() => new Map(fields.map((f) => [f.name, f])), [fields]);
2303
+ const activeSavedQuery = useMemo4(
1478
2304
  () => api.saved.items.find((item) => queriesEqual(item.query, api.query)),
1479
2305
  [api.query, api.saved.items]
1480
2306
  );
1481
- const lastSavedQuery = useMemo3(
2307
+ const lastSavedQuery = useMemo4(
1482
2308
  () => api.saved.items.find((item) => item.id === lastSavedId) ?? null,
1483
2309
  [api.saved.items, lastSavedId]
1484
2310
  );
1485
2311
  const isSavedQuery = activeSavedQuery != null;
1486
2312
  const isEditingUnsaved = editingSavedId === UNSAVED_QUERY_EDIT_ID;
1487
- const bodyId = useId();
2313
+ const bodyId = useId2();
1488
2314
  const isEditingSaved = editingSavedId != null && activeSavedQuery?.id === editingSavedId;
1489
2315
  const showUpdateButton = Boolean(lastSavedQuery) && !activeSavedQuery;
1490
- const collapsedSummary = useMemo3(
2316
+ const collapsedSummary = useMemo4(
1491
2317
  () => buildCollapsedSummary(api.query.where, api.query.orderBy, byName),
1492
2318
  [api.query.where, api.query.orderBy, byName]
1493
2319
  );
1494
- const collapsedWhereText = useMemo3(
2320
+ const collapsedWhereText = useMemo4(
1495
2321
  () => whereClauseSummaryText(api.query.where, byName),
1496
2322
  [api.query.where, byName]
1497
2323
  );
1498
- const collapsedOrderText = useMemo3(
2324
+ const collapsedOrderText = useMemo4(
1499
2325
  () => orderBySummaryText(api.query.orderBy, byName),
1500
2326
  [api.query.orderBy, byName]
1501
2327
  );
@@ -1507,27 +2333,27 @@ function QueryBuilder({
1507
2333
  const lastUpdatedText = formatStatusTime(lastUpdatedAt);
1508
2334
  const failureText = api.error ? `Error: ${api.error.message}` : lastFailureAt ? `Last failure at ${formatStatusTime(lastFailureAt)}` : null;
1509
2335
  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(
2336
+ const lastRowsRef = useRef3(null);
2337
+ const lastTotalRef = useRef3(null);
2338
+ const previousLoadingRef = useRef3(api.loading);
2339
+ const autoRefreshRef = useRef3(null);
2340
+ const autoRefreshPopoverRef = useRef3(null);
2341
+ const [autoRefreshPopoverAlignRight, setAutoRefreshPopoverAlignRight] = useState5(false);
2342
+ const shareFeedbackTimerRef = useRef3(null);
2343
+ useEffect5(
1518
2344
  () => () => {
1519
2345
  if (shareFeedbackTimerRef.current) clearTimeout(shareFeedbackTimerRef.current);
1520
2346
  },
1521
2347
  []
1522
2348
  );
1523
- useEffect4(() => {
2349
+ useEffect5(() => {
1524
2350
  setShareCopied(false);
1525
2351
  if (shareFeedbackTimerRef.current) {
1526
2352
  clearTimeout(shareFeedbackTimerRef.current);
1527
2353
  shareFeedbackTimerRef.current = null;
1528
2354
  }
1529
2355
  }, [api.query]);
1530
- useEffect4(() => {
2356
+ useEffect5(() => {
1531
2357
  if (activeSavedQuery) {
1532
2358
  setLastSavedId(activeSavedQuery.id);
1533
2359
  if (!isEditingSaved) {
@@ -1541,11 +2367,11 @@ function QueryBuilder({
1541
2367
  setEditingSavedName("");
1542
2368
  }
1543
2369
  }, [activeSavedQuery, isEditingSaved, isEditingUnsaved]);
1544
- useEffect4(() => {
2370
+ useEffect5(() => {
1545
2371
  if (!lastSavedId || api.saved.items.some((item) => item.id === lastSavedId)) return;
1546
2372
  setLastSavedId(null);
1547
2373
  }, [api.saved.items, lastSavedId]);
1548
- useEffect4(() => {
2374
+ useEffect5(() => {
1549
2375
  const hasData = api.rows.length > 0 || total != null;
1550
2376
  const rowsChanged = lastRowsRef.current !== api.rows;
1551
2377
  const totalChanged = lastTotalRef.current !== total;
@@ -1640,15 +2466,15 @@ function QueryBuilder({
1640
2466
  return;
1641
2467
  }
1642
2468
  }
1643
- useEffect4(() => {
2469
+ useEffect5(() => {
1644
2470
  if (isCollapsed) setShowSaved(false);
1645
2471
  }, [isCollapsed]);
1646
- useEffect4(() => {
2472
+ useEffect5(() => {
1647
2473
  if (!showAutoRefresh || !autoRefreshStatus) return;
1648
2474
  setAutoRefreshFrequencyMs(autoRefreshStatus.frequencyMs);
1649
2475
  setAutoRefreshTurnOffAfterMs(autoRefreshStatus.turnOffAfterMs);
1650
2476
  }, [showAutoRefresh, autoRefreshStatus]);
1651
- useEffect4(() => {
2477
+ useEffect5(() => {
1652
2478
  if (!showAutoRefresh) return;
1653
2479
  function onDocumentClick(event) {
1654
2480
  const wrapper = autoRefreshRef.current;
@@ -1707,11 +2533,11 @@ function QueryBuilder({
1707
2533
  window.prompt("Copy this query URL", url.toString());
1708
2534
  }
1709
2535
  }
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(
2536
+ return /* @__PURE__ */ jsxs6("div", { className: cx3("qt-qb", classNames?.root), children: [
2537
+ /* @__PURE__ */ jsxs6("div", { className: "qt-qb-bar qt-qb-state-bar qt-qb-row", children: [
2538
+ /* @__PURE__ */ jsxs6("span", { className: "qt-qb-state-main", children: [
2539
+ /* @__PURE__ */ jsxs6("span", { className: "qt-qb-saved", children: [
2540
+ /* @__PURE__ */ jsx6(
1715
2541
  "button",
1716
2542
  {
1717
2543
  type: "button",
@@ -1722,8 +2548,8 @@ function QueryBuilder({
1722
2548
  children: isSavedQuery ? "\u2605" : "\u2606"
1723
2549
  }
1724
2550
  ),
1725
- isEditingSaved || isEditingUnsaved ? /* @__PURE__ */ jsxs5("span", { className: "qt-qb-saved-input-wrap", children: [
1726
- /* @__PURE__ */ jsx5(
2551
+ isEditingSaved || isEditingUnsaved ? /* @__PURE__ */ jsxs6("span", { className: "qt-qb-saved-input-wrap", children: [
2552
+ /* @__PURE__ */ jsx6(
1727
2553
  "input",
1728
2554
  {
1729
2555
  type: "text",
@@ -1746,7 +2572,7 @@ function QueryBuilder({
1746
2572
  onClick: (e) => e.stopPropagation()
1747
2573
  }
1748
2574
  ),
1749
- /* @__PURE__ */ jsx5(
2575
+ /* @__PURE__ */ jsx6(
1750
2576
  "button",
1751
2577
  {
1752
2578
  type: "button",
@@ -1760,7 +2586,7 @@ function QueryBuilder({
1760
2586
  children: "\u2715"
1761
2587
  }
1762
2588
  )
1763
- ] }) : /* @__PURE__ */ jsx5(
2589
+ ] }) : /* @__PURE__ */ jsx6(
1764
2590
  "button",
1765
2591
  {
1766
2592
  type: "button",
@@ -1770,17 +2596,17 @@ function QueryBuilder({
1770
2596
  }
1771
2597
  )
1772
2598
  ] }),
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" }),
2599
+ isCollapsed ? /* @__PURE__ */ jsxs6("span", { className: "qt-qb-summary qt-truncate", title: collapsedSummary, children: [
2600
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-summary-kw", children: "WHERE" }),
1775
2601
  " ",
1776
- /* @__PURE__ */ jsx5("strong", { children: collapsedWhereText }),
2602
+ /* @__PURE__ */ jsx6("strong", { children: collapsedWhereText }),
1777
2603
  " ",
1778
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-summary-kw", children: "ORDER BY" }),
2604
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-summary-kw", children: "ORDER BY" }),
1779
2605
  " ",
1780
- /* @__PURE__ */ jsx5("strong", { children: collapsedOrderText })
2606
+ /* @__PURE__ */ jsx6("strong", { children: collapsedOrderText })
1781
2607
  ] }) : null
1782
2608
  ] }),
1783
- showUpdateButton ? /* @__PURE__ */ jsx5(
2609
+ showUpdateButton ? /* @__PURE__ */ jsx6(
1784
2610
  "button",
1785
2611
  {
1786
2612
  type: "button",
@@ -1790,7 +2616,7 @@ function QueryBuilder({
1790
2616
  children: `Update Query '${lastSavedQuery?.name ?? "query"}'`
1791
2617
  }
1792
2618
  ) : null,
1793
- /* @__PURE__ */ jsx5(
2619
+ /* @__PURE__ */ jsx6(
1794
2620
  "button",
1795
2621
  {
1796
2622
  type: "button",
@@ -1801,11 +2627,11 @@ function QueryBuilder({
1801
2627
  children: shareCopied ? "Copied!" : "Share"
1802
2628
  }
1803
2629
  ),
1804
- /* @__PURE__ */ jsxs5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: () => setShowSaved(true), children: [
2630
+ /* @__PURE__ */ jsxs6("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: () => setShowSaved(true), children: [
1805
2631
  "Saved",
1806
2632
  api.saved.items.length > 0 ? ` (${api.saved.items.length})` : ""
1807
2633
  ] }),
1808
- /* @__PURE__ */ jsxs5(
2634
+ /* @__PURE__ */ jsxs6(
1809
2635
  "button",
1810
2636
  {
1811
2637
  type: "button",
@@ -1815,7 +2641,7 @@ function QueryBuilder({
1815
2641
  "aria-controls": bodyId,
1816
2642
  title: isCollapsed ? "Expand query builder" : "Collapse query builder",
1817
2643
  children: [
1818
- /* @__PURE__ */ jsx5(
2644
+ /* @__PURE__ */ jsx6(
1819
2645
  "span",
1820
2646
  {
1821
2647
  className: cx3("qt-qb-collapse-caret", isCollapsed && "qt-qb-collapse-caret--collapsed"),
@@ -1823,38 +2649,38 @@ function QueryBuilder({
1823
2649
  children: "\u25BE"
1824
2650
  }
1825
2651
  ),
1826
- /* @__PURE__ */ jsx5("span", { className: "qt-sr-only", children: isCollapsed ? "Expand query builder" : "Collapse query builder" })
2652
+ /* @__PURE__ */ jsx6("span", { className: "qt-sr-only", children: isCollapsed ? "Expand query builder" : "Collapse query builder" })
1827
2653
  ]
1828
2654
  }
1829
2655
  )
1830
2656
  ] }),
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 })
2657
+ !isCollapsed ? /* @__PURE__ */ jsxs6("div", { className: "qt-qb-bar qt-qb-editor-bar", children: [
2658
+ /* @__PURE__ */ jsxs6("div", { id: bodyId, className: "qt-qb-body", children: [
2659
+ /* @__PURE__ */ jsx6(SelectRow, { api, fields, classNames, disabled: isBusy }),
2660
+ /* @__PURE__ */ jsx6(WhereRow, { api, byName, fields, classNames, disabled: isBusy }),
2661
+ /* @__PURE__ */ jsx6(OrderRow, { api, fields, classNames, disabled: isBusy }),
2662
+ /* @__PURE__ */ jsx6(WindowRow, { api, classNames, disabled: isBusy }),
2663
+ /* @__PURE__ */ jsx6(MetricsRow, { api, fields, classNames, disabled: isBusy })
1838
2664
  ] }),
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" })
2665
+ /* @__PURE__ */ jsxs6("div", { className: "qt-qb-editor-stack", children: [
2666
+ /* @__PURE__ */ jsx6("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.undo, disabled: !api.canUndo || isBusy, children: "\u21B6 Undo" }),
2667
+ /* @__PURE__ */ jsx6("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.redo, disabled: !api.canRedo || isBusy, children: "\u21B7 Redo" }),
2668
+ /* @__PURE__ */ jsx6("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.resetAll, disabled: isBusy, children: "Reset All" })
1843
2669
  ] })
1844
2670
  ] }) : 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: [
2671
+ /* @__PURE__ */ jsxs6("div", { className: "qt-qb-bar qt-qb-run-bar qt-qb-row", children: [
2672
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-run-metric qt-qb-run-metric--strong", children: api.loading ? "loading\u2026" : `${rowSummary} rows` }),
2673
+ /* @__PURE__ */ jsxs6("span", { className: "qt-qb-run-metric", children: [
1848
2674
  "Last updated ",
1849
2675
  lastUpdatedText
1850
2676
  ] }),
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(
2677
+ failureText ? /* @__PURE__ */ jsx6("span", { className: "qt-qb-run-error", children: failureText }) : null,
2678
+ /* @__PURE__ */ jsxs6("span", { className: "qt-qb-run-actions", children: [
2679
+ autoRefreshStatus ? /* @__PURE__ */ jsx6("span", { className: "qt-qb-auto-status", children: `Every ${formatDuration2(autoRefreshStatus.frequencyMs)}; clear after ${formatDuration2(
1854
2680
  autoRefreshStatus.turnOffAfterMs
1855
2681
  )} (${autoRefreshStatus.pollCount} polls run)` }) : null,
1856
- /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh", ref: autoRefreshRef, children: [
1857
- /* @__PURE__ */ jsx5(
2682
+ /* @__PURE__ */ jsxs6("span", { className: "qt-auto-refresh", ref: autoRefreshRef, children: [
2683
+ /* @__PURE__ */ jsx6(
1858
2684
  "button",
1859
2685
  {
1860
2686
  type: "button",
@@ -1865,7 +2691,7 @@ function QueryBuilder({
1865
2691
  children: autoRefreshButtonText
1866
2692
  }
1867
2693
  ),
1868
- showAutoRefresh ? /* @__PURE__ */ jsxs5(
2694
+ showAutoRefresh ? /* @__PURE__ */ jsxs6(
1869
2695
  "span",
1870
2696
  {
1871
2697
  ref: autoRefreshPopoverRef,
@@ -1873,27 +2699,27 @@ function QueryBuilder({
1873
2699
  role: "dialog",
1874
2700
  "aria-label": "Auto-refresh settings",
1875
2701
  children: [
1876
- /* @__PURE__ */ jsxs5("label", { className: "qt-auto-refresh-field", children: [
1877
- /* @__PURE__ */ jsx5("span", { children: "Frequency" }),
1878
- /* @__PURE__ */ jsx5(
2702
+ /* @__PURE__ */ jsxs6("label", { className: "qt-auto-refresh-field", children: [
2703
+ /* @__PURE__ */ jsx6("span", { children: "Frequency" }),
2704
+ /* @__PURE__ */ jsx6(
1879
2705
  "select",
1880
2706
  {
1881
2707
  className: "qt-auto-refresh-select",
1882
2708
  value: autoRefreshFrequencyMs,
1883
2709
  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))
2710
+ children: AUTO_REFRESH_FREQUENCIES.map((option) => /* @__PURE__ */ jsx6("option", { value: option.value, children: option.label }, option.value))
1885
2711
  }
1886
2712
  )
1887
2713
  ] }),
1888
- /* @__PURE__ */ jsxs5("label", { className: "qt-auto-refresh-field", children: [
1889
- /* @__PURE__ */ jsx5("span", { children: "Turn off after" }),
1890
- /* @__PURE__ */ jsx5(
2714
+ /* @__PURE__ */ jsxs6("label", { className: "qt-auto-refresh-field", children: [
2715
+ /* @__PURE__ */ jsx6("span", { children: "Turn off after" }),
2716
+ /* @__PURE__ */ jsx6(
1891
2717
  "select",
1892
2718
  {
1893
2719
  className: "qt-auto-refresh-select",
1894
2720
  value: autoRefreshTurnOffAfterMs,
1895
2721
  onChange: (e) => setAutoRefreshTurnOffAfterMs(Number(e.target.value)),
1896
- children: AUTO_REFRESH_TURN_OFF_AFTER.map((option) => /* @__PURE__ */ jsx5(
2722
+ children: AUTO_REFRESH_TURN_OFF_AFTER.map((option) => /* @__PURE__ */ jsx6(
1897
2723
  "option",
1898
2724
  {
1899
2725
  value: option.value,
@@ -1905,8 +2731,8 @@ function QueryBuilder({
1905
2731
  }
1906
2732
  )
1907
2733
  ] }),
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: [
2734
+ /* @__PURE__ */ jsx6("span", { className: cx3("qt-auto-refresh-status", !canSubmitAutoRefresh && "qt-auto-refresh-status--error"), children: canSubmitAutoRefresh ? `${selectedAutoRefreshPolls} polls maximum` : `Choose between 1 and 1000 polls` }),
2735
+ autoRefreshStatus ? /* @__PURE__ */ jsxs6("span", { className: "qt-auto-refresh-status", children: [
1910
2736
  "Active: every ",
1911
2737
  formatDuration2(autoRefreshStatus.frequencyMs),
1912
2738
  ", clears after",
@@ -1916,8 +2742,8 @@ function QueryBuilder({
1916
2742
  autoRefreshStatus.pollCount,
1917
2743
  " polls run."
1918
2744
  ] }) : null,
1919
- /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh-actions", children: [
1920
- /* @__PURE__ */ jsx5(
2745
+ /* @__PURE__ */ jsxs6("span", { className: "qt-auto-refresh-actions", children: [
2746
+ /* @__PURE__ */ jsx6(
1921
2747
  "button",
1922
2748
  {
1923
2749
  type: "button",
@@ -1927,7 +2753,7 @@ function QueryBuilder({
1927
2753
  children: autoRefreshStatus ? "Update" : "Start"
1928
2754
  }
1929
2755
  ),
1930
- autoRefreshStatus ? /* @__PURE__ */ jsx5(
2756
+ autoRefreshStatus ? /* @__PURE__ */ jsx6(
1931
2757
  "button",
1932
2758
  {
1933
2759
  type: "button",
@@ -1944,7 +2770,7 @@ function QueryBuilder({
1944
2770
  }
1945
2771
  ) : null
1946
2772
  ] }),
1947
- /* @__PURE__ */ jsx5(
2773
+ /* @__PURE__ */ jsx6(
1948
2774
  "button",
1949
2775
  {
1950
2776
  type: "button",
@@ -1956,7 +2782,7 @@ function QueryBuilder({
1956
2782
  )
1957
2783
  ] })
1958
2784
  ] }),
1959
- !isCollapsed && showSaved ? /* @__PURE__ */ jsx5(SavedQueriesModal, { saved: api.saved, onClose: () => setShowSaved(false), onLoad: setLastSavedId }) : null
2785
+ !isCollapsed && showSaved ? /* @__PURE__ */ jsx6(SavedQueriesModal, { saved: api.saved, onClose: () => setShowSaved(false), onLoad: setLastSavedId }) : null
1960
2786
  ] });
1961
2787
  }
1962
2788
  function MoveButtons({
@@ -1967,8 +2793,8 @@ function MoveButtons({
1967
2793
  onMove
1968
2794
  }) {
1969
2795
  if (count < 2) return null;
1970
- return /* @__PURE__ */ jsxs5("span", { className: "qt-chip-move-buttons", children: [
1971
- /* @__PURE__ */ jsx5(
2796
+ return /* @__PURE__ */ jsxs6("span", { className: "qt-chip-move-buttons", children: [
2797
+ /* @__PURE__ */ jsx6(
1972
2798
  "button",
1973
2799
  {
1974
2800
  type: "button",
@@ -1980,7 +2806,7 @@ function MoveButtons({
1980
2806
  children: "\u2190"
1981
2807
  }
1982
2808
  ),
1983
- /* @__PURE__ */ jsx5(
2809
+ /* @__PURE__ */ jsx6(
1984
2810
  "button",
1985
2811
  {
1986
2812
  type: "button",
@@ -2001,19 +2827,22 @@ function SelectRow({
2001
2827
  disabled
2002
2828
  }) {
2003
2829
  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]);
2830
+ const [editingColumns, setEditingColumns] = useState5(false);
2831
+ const [adding, setAdding] = useState5(false);
2832
+ const fieldLabelByName = useMemo4(() => new Map(select.fields.map((f) => [f.name, f.label])), [select.fields]);
2006
2833
  function reorderByIndex(from, overIndex) {
2007
2834
  select.move(from, overIndex);
2008
2835
  }
2009
2836
  const fieldNames = select.visible.map((c) => c.field);
2010
2837
  const dragSource = columnDrag.source;
2011
2838
  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" }),
2839
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-qb-row", children: [
2840
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "select" }),
2841
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-btn", disabled, onClick: () => setEditingColumns(true), children: "Edit columns" }),
2842
+ editingColumns && /* @__PURE__ */ jsx6(SelectColumnEditor, { api, onClose: () => setEditingColumns(false) }),
2014
2843
  rendered.map((name, position) => {
2015
2844
  const label = fieldLabelByName.get(name) ?? name;
2016
- return /* @__PURE__ */ jsxs5(
2845
+ return /* @__PURE__ */ jsxs6(
2017
2846
  "span",
2018
2847
  {
2019
2848
  className: cx3(
@@ -2052,9 +2881,9 @@ function SelectRow({
2052
2881
  onDragEnd: () => columnDrag.end(),
2053
2882
  title: "drag to reorder",
2054
2883
  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(
2884
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2885
+ /* @__PURE__ */ jsx6("span", { className: "qt-chip-field", children: label }),
2886
+ /* @__PURE__ */ jsx6(
2058
2887
  MoveButtons,
2059
2888
  {
2060
2889
  label,
@@ -2064,23 +2893,23 @@ function SelectRow({
2064
2893
  onMove: (nextPosition) => select.move(name, nextPosition)
2065
2894
  }
2066
2895
  ),
2067
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => select.hide(name), disabled, children: "\u2715" })
2896
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-chip-x", onClick: () => select.hide(name), disabled, children: "\u2715" })
2068
2897
  ]
2069
2898
  },
2070
2899
  name
2071
2900
  );
2072
2901
  }),
2073
- adding ? /* @__PURE__ */ jsx5(
2902
+ adding ? /* @__PURE__ */ jsx6(
2074
2903
  FieldPicker,
2075
2904
  {
2076
- fields: select.hidden.filter(isSelectable),
2905
+ fields: select.hidden.filter(isSelectable2),
2077
2906
  onPick: (f) => {
2078
2907
  select.show(f.name);
2079
2908
  setAdding(false);
2080
2909
  },
2081
2910
  onClose: () => setAdding(false)
2082
2911
  }
2083
- ) : /* @__PURE__ */ jsx5(
2912
+ ) : /* @__PURE__ */ jsx6(
2084
2913
  "button",
2085
2914
  {
2086
2915
  type: "button",
@@ -2090,9 +2919,16 @@ function SelectRow({
2090
2919
  children: "+ add column"
2091
2920
  }
2092
2921
  ),
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" })
2922
+ api.query.select.length > 0 && /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-link-btn", onClick: select.reset, disabled, title: "Reset to default columns", children: "reset" })
2094
2923
  ] });
2095
2924
  }
2925
+ function dropModeFor(e) {
2926
+ const rect = e.currentTarget.getBoundingClientRect();
2927
+ const x = e.clientX - rect.left;
2928
+ if (x < rect.width * 0.3) return "before";
2929
+ if (x > rect.width * 0.7) return "after";
2930
+ return "merge";
2931
+ }
2096
2932
  function WhereRow({
2097
2933
  api,
2098
2934
  byName,
@@ -2100,120 +2936,295 @@ function WhereRow({
2100
2936
  classNames,
2101
2937
  disabled
2102
2938
  }) {
2103
- const [adding, setAdding] = useState4(false);
2939
+ const [adding, setAdding] = useState5(false);
2940
+ const [drag, setDrag] = useState5(null);
2941
+ const where = api.query.where;
2942
+ const hasOr = where.some(isOrGroup);
2104
2943
  function addClause(f) {
2105
- const op = opsForField2(f)[0] ?? "=";
2944
+ const op = positiveOpsForField(f)[0] ?? opsForField2(f)[0] ?? "=";
2106
2945
  api.addFilter({ field: f.name, op, value: "" });
2107
2946
  setAdding(false);
2108
2947
  }
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(
2948
+ function handleOver(e, index) {
2949
+ if (!drag) return;
2950
+ e.preventDefault();
2951
+ e.dataTransfer.dropEffect = "move";
2952
+ const mode = dropModeFor(e);
2953
+ setDrag((d) => d && (d.over?.index !== index || d.over?.mode !== mode) ? { from: d.from, over: { index, mode } } : d);
2954
+ }
2955
+ function handleDrop() {
2956
+ if (!drag || !drag.over) {
2957
+ setDrag(null);
2958
+ return;
2959
+ }
2960
+ const { from } = drag;
2961
+ const { index: target, mode } = drag.over;
2962
+ if (mode === "merge") {
2963
+ if (target !== from) api.mergeFilters(from, target);
2964
+ } else {
2965
+ const tPrime = target < from ? target : target - 1;
2966
+ const insertAt = mode === "after" ? tPrime + 1 : tPrime;
2967
+ if (insertAt !== from) api.reorderFilters(from, insertAt);
2968
+ }
2969
+ setDrag(null);
2970
+ }
2971
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-qb-row", children: [
2972
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "where" }),
2973
+ where.length === 0 && !adding && /* @__PURE__ */ jsx6("span", { className: "qt-qb-hint", children: "all rows" }),
2974
+ where.map((term, index) => /* @__PURE__ */ jsxs6("span", { className: "qt-where-term-wrap", children: [
2975
+ index > 0 && hasOr && /* @__PURE__ */ jsx6("span", { className: "qt-where-and", children: "AND" }),
2976
+ /* @__PURE__ */ jsx6(
2977
+ TermChip,
2978
+ {
2979
+ api,
2980
+ term,
2981
+ index,
2982
+ byName,
2983
+ classNames,
2984
+ disabled,
2985
+ isDragSource: drag?.from === index,
2986
+ dropHint: drag?.over?.index === index ? drag.over.mode : null,
2987
+ onDragStart: () => setDrag({ from: index, over: null }),
2988
+ onDragOver: (e) => handleOver(e, index),
2989
+ onDrop: handleDrop,
2990
+ onDragEnd: () => setDrag(null)
2991
+ }
2992
+ )
2993
+ ] }, index)),
2994
+ adding ? /* @__PURE__ */ jsx6(
2137
2995
  FieldPicker,
2138
2996
  {
2139
- fields: fields.filter(isFilterable),
2997
+ fields: fields.filter(isFilterable2),
2140
2998
  gateOnDistinct: true,
2141
2999
  onPick: addClause,
2142
3000
  onClose: () => setAdding(false)
2143
3001
  }
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" })
3002
+ ) : /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add filter" }),
3003
+ where.length > 0 && /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-link-btn", onClick: api.clearFilters, disabled, title: "Reset filters", children: "reset" })
2146
3004
  ] });
2147
3005
  }
2148
- function ClauseChip({
3006
+ function TermChip({
2149
3007
  api,
2150
- field,
2151
- clauses,
3008
+ term,
3009
+ index,
3010
+ byName,
2152
3011
  classNames,
2153
3012
  disabled,
2154
- onChange,
2155
- onRemove
3013
+ isDragSource,
3014
+ dropHint,
3015
+ onDragStart,
3016
+ onDragOver,
3017
+ onDrop,
3018
+ onDragEnd
2156
3019
  }) {
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",
3020
+ const group = isOrGroup(term);
3021
+ const predicates = predicatesOf(term);
3022
+ const grip = /* @__PURE__ */ jsx6(
3023
+ "span",
3024
+ {
3025
+ className: "qt-chip-grip",
3026
+ "aria-hidden": true,
3027
+ draggable: !disabled,
3028
+ onDragStart: (e) => {
3029
+ onDragStart();
3030
+ e.dataTransfer.effectAllowed = "move";
3031
+ e.dataTransfer.setData("text/plain", String(index));
3032
+ },
3033
+ onDragEnd,
3034
+ title: "drag to reorder, or drop onto another filter to make an OR",
3035
+ children: "\u22EE\u22EE"
3036
+ }
3037
+ );
3038
+ return /* @__PURE__ */ jsxs6(
3039
+ "span",
3040
+ {
3041
+ className: cx3(
3042
+ "qt-chip",
3043
+ "qt-chip--where",
3044
+ group && "qt-chip--or",
3045
+ isDragSource && "qt-chip--dragging",
3046
+ dropHint ? `qt-chip--drop-${dropHint}` : void 0,
3047
+ classNames?.chip
3048
+ ),
3049
+ onDragOver,
3050
+ onDrop: (e) => {
3051
+ e.preventDefault();
3052
+ onDrop();
3053
+ },
3054
+ children: [
3055
+ grip,
3056
+ group ? /* @__PURE__ */ jsx6("span", { className: "qt-or-group", children: predicates.map((clause, predIndex) => /* @__PURE__ */ jsxs6("span", { className: "qt-or-member", children: [
3057
+ predIndex > 0 && /* @__PURE__ */ jsx6("span", { className: "qt-chip-and qt-chip-or", children: "OR" }),
3058
+ /* @__PURE__ */ jsx6(
3059
+ PredicateEditor,
3060
+ {
3061
+ api,
3062
+ field: byName.get(clause.field),
3063
+ clause,
3064
+ classNames,
3065
+ disabled,
3066
+ onChange: (next) => api.updatePredicate(index, predIndex, next),
3067
+ onRemove: () => api.removePredicate(index, predIndex)
3068
+ }
3069
+ )
3070
+ ] }, predIndex)) }) : /* @__PURE__ */ jsx6(
3071
+ PredicateEditor,
2172
3072
  {
2173
- className: cx3("qt-chip-op", classNames?.select),
2174
- value: clause.op,
3073
+ api,
3074
+ field: byName.get(term.field),
3075
+ clause: term,
3076
+ classNames,
2175
3077
  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))
3078
+ onChange: (next) => api.updatePredicate(index, 0, next),
3079
+ onRemove: () => api.removeFilter(index)
2179
3080
  }
2180
- ),
2181
- needsValue && /* @__PURE__ */ jsx5(
2182
- ValueInput,
3081
+ )
3082
+ ]
3083
+ }
3084
+ );
3085
+ }
3086
+ var OP_LABEL = {
3087
+ "=": "=",
3088
+ "!=": "\u2260",
3089
+ ">": ">",
3090
+ ">=": "\u2265",
3091
+ "<": "<",
3092
+ "<=": "\u2264",
3093
+ contains: "contains",
3094
+ starts_with: "starts with",
3095
+ ends_with: "ends with",
3096
+ matches_regex: "matches",
3097
+ not_matches_regex: "not matches",
3098
+ includes: "includes",
3099
+ is_null: "is null",
3100
+ is_not_null: "is not null"
3101
+ };
3102
+ function choiceLabel(op, negated) {
3103
+ return negated ? `not ${OP_LABEL[op]}` : OP_LABEL[op];
3104
+ }
3105
+ function OpPicker({
3106
+ field,
3107
+ clause,
3108
+ disabled,
3109
+ classNames,
3110
+ onPick
3111
+ }) {
3112
+ const [open, setOpen] = useState5(false);
3113
+ const wrapRef = useRef3(null);
3114
+ useEffect5(() => {
3115
+ if (!open) return;
3116
+ function onDocClick(e) {
3117
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
3118
+ }
3119
+ function onKey(e) {
3120
+ if (e.key === "Escape") setOpen(false);
3121
+ }
3122
+ const t = setTimeout(() => window.addEventListener("click", onDocClick), 0);
3123
+ window.addEventListener("keydown", onKey);
3124
+ return () => {
3125
+ clearTimeout(t);
3126
+ window.removeEventListener("click", onDocClick);
3127
+ window.removeEventListener("keydown", onKey);
3128
+ };
3129
+ }, [open]);
3130
+ const pairs = useMemo4(() => field ? opPairsForField2(field) : [], [field]);
3131
+ const isActive = (choice) => choice.op === clause.op && Boolean(choice.negated) === Boolean(clause.negated);
3132
+ return /* @__PURE__ */ jsxs6("span", { className: "qt-op-picker", ref: wrapRef, children: [
3133
+ /* @__PURE__ */ jsxs6(
3134
+ "button",
3135
+ {
3136
+ type: "button",
3137
+ className: cx3("qt-chip-op-btn", "qt-op-trigger", classNames?.select),
3138
+ disabled,
3139
+ "aria-haspopup": "dialog",
3140
+ "aria-expanded": open,
3141
+ onClick: () => setOpen((o) => !o),
3142
+ children: [
3143
+ choiceLabel(clause.op, Boolean(clause.negated)),
3144
+ /* @__PURE__ */ jsx6("span", { className: "qt-op-caret", "aria-hidden": true, children: "\u25BE" })
3145
+ ]
3146
+ }
3147
+ ),
3148
+ open && pairs.length > 0 && /* @__PURE__ */ jsxs6("span", { className: "qt-op-pop", role: "dialog", "aria-label": "Choose condition", children: [
3149
+ /* @__PURE__ */ jsx6("span", { className: "qt-cm-col-head", children: "keep" }),
3150
+ /* @__PURE__ */ jsx6("span", { className: "qt-cm-col-head qt-cm-col-head--neg", children: "exclude" }),
3151
+ pairs.map((pair, i) => /* @__PURE__ */ jsxs6(Fragment5, { children: [
3152
+ /* @__PURE__ */ jsx6(
3153
+ "button",
2183
3154
  {
2184
- api,
2185
- field,
2186
- value: clause.value,
2187
- classNames,
2188
- onChange: (v) => onChange(entry.index, { ...clause, value: v })
3155
+ type: "button",
3156
+ className: cx3("qt-op-cell", isActive(pair.keep) && "qt-op-cell--on"),
3157
+ onClick: () => {
3158
+ onPick(pair.keep);
3159
+ setOpen(false);
3160
+ },
3161
+ children: choiceLabel(pair.keep.op, pair.keep.negated)
2189
3162
  }
2190
3163
  ),
2191
- /* @__PURE__ */ jsx5(
3164
+ /* @__PURE__ */ jsx6(
2192
3165
  "button",
2193
3166
  {
2194
3167
  type: "button",
2195
- className: "qt-chip-x",
2196
- onClick: () => onRemove(entry.index),
2197
- disabled,
2198
- children: "\u2715"
3168
+ className: cx3("qt-op-cell", "qt-op-cell--neg", isActive(pair.exclude) && "qt-op-cell--on"),
3169
+ onClick: () => {
3170
+ onPick(pair.exclude);
3171
+ setOpen(false);
3172
+ },
3173
+ children: choiceLabel(pair.exclude.op, pair.exclude.negated)
2199
3174
  }
2200
3175
  )
2201
- ] }, entry.index);
2202
- }) })
3176
+ ] }, i))
3177
+ ] })
3178
+ ] });
3179
+ }
3180
+ function PredicateEditor({
3181
+ api,
3182
+ field,
3183
+ clause,
3184
+ classNames,
3185
+ disabled,
3186
+ onChange,
3187
+ onRemove
3188
+ }) {
3189
+ const needsValue = !NULLARY_OPS2.has(clause.op);
3190
+ const isRegex = clause.op === "matches_regex" || clause.op === "not_matches_regex";
3191
+ function pickOp(choice) {
3192
+ const next = { field: clause.field, op: choice.op, value: clause.value };
3193
+ if (choice.negated) next.negated = true;
3194
+ onChange(next);
3195
+ }
3196
+ return /* @__PURE__ */ jsxs6("span", { className: "qt-pred", children: [
3197
+ /* @__PURE__ */ jsx6("span", { className: "qt-chip-field", children: field?.label ?? clause.field }),
3198
+ /* @__PURE__ */ jsx6(OpPicker, { field, clause, disabled, classNames, onPick: pickOp }),
3199
+ needsValue && /* @__PURE__ */ jsx6(
3200
+ ValueInput,
3201
+ {
3202
+ api,
3203
+ field,
3204
+ value: clause.value,
3205
+ forceFreeform: isRegex,
3206
+ placeholder: isRegex ? "regex" : "value",
3207
+ classNames,
3208
+ onChange: (v) => onChange({ ...clause, value: v })
3209
+ }
3210
+ ),
3211
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-chip-x", onClick: onRemove, disabled, children: "\u2715" })
2203
3212
  ] });
2204
3213
  }
2205
3214
  function ValueInput({
2206
3215
  api,
2207
3216
  field,
2208
3217
  value,
3218
+ forceFreeform,
3219
+ placeholder = "value",
2209
3220
  classNames,
2210
3221
  onChange
2211
3222
  }) {
2212
3223
  const strategy = field ? filterValuesFor(field) : { source: "freeform" };
2213
- if (strategy.source === "static") {
3224
+ if (!forceFreeform && strategy.source === "static") {
2214
3225
  const selectedText = value || "\u2014";
2215
3226
  const widthChars = chipFieldWidthForSelect(selectedText, 4);
2216
- return /* @__PURE__ */ jsxs5(
3227
+ return /* @__PURE__ */ jsxs6(
2217
3228
  "select",
2218
3229
  {
2219
3230
  className: cx3("qt-chip-val", classNames?.select),
@@ -2221,27 +3232,27 @@ function ValueInput({
2221
3232
  style: { width: `${widthChars}ch` },
2222
3233
  onChange: (e) => onChange(e.target.value),
2223
3234
  children: [
2224
- /* @__PURE__ */ jsx5("option", { value: "", children: "\u2014" }),
2225
- strategy.options.map((o) => /* @__PURE__ */ jsx5("option", { value: o, children: o }, o))
3235
+ /* @__PURE__ */ jsx6("option", { value: "", children: "\u2014" }),
3236
+ strategy.options.map((o) => /* @__PURE__ */ jsx6("option", { value: o, children: o }, o))
2226
3237
  ]
2227
3238
  }
2228
3239
  );
2229
3240
  }
2230
- if (strategy.source === "freeform" || !field) {
2231
- const widthChars = chipFieldWidthForInput(value || "value", field?.type, 5, 4);
2232
- return /* @__PURE__ */ jsx5(
3241
+ if (forceFreeform || strategy.source === "freeform" || !field) {
3242
+ const widthChars = chipFieldWidthForInput(value || placeholder, field?.type, 5, 4);
3243
+ return /* @__PURE__ */ jsx6(
2233
3244
  "input",
2234
3245
  {
2235
3246
  className: cx3("qt-chip-val", classNames?.input),
2236
3247
  value,
2237
- placeholder: "value",
3248
+ placeholder,
2238
3249
  size: widthChars,
2239
3250
  style: { width: `${widthChars}ch` },
2240
3251
  onChange: (e) => onChange(e.target.value)
2241
3252
  }
2242
3253
  );
2243
3254
  }
2244
- return /* @__PURE__ */ jsx5(AutocompleteInput, { api, field, value, classNames, onChange });
3255
+ return /* @__PURE__ */ jsx6(AutocompleteInput, { api, field, value, classNames, onChange });
2245
3256
  }
2246
3257
  function AutocompleteInput({
2247
3258
  api,
@@ -2250,9 +3261,9 @@ function AutocompleteInput({
2250
3261
  classNames,
2251
3262
  onChange
2252
3263
  }) {
2253
- const [result, setResult] = useState4({ values: [], hasMore: false });
2254
- const listId = useId();
2255
- useEffect4(() => {
3264
+ const [result, setResult] = useState5({ values: [], hasMore: false });
3265
+ const listId = useId2();
3266
+ useEffect5(() => {
2256
3267
  let cancelled = false;
2257
3268
  const t = setTimeout(() => {
2258
3269
  void api.filterValues(field.name, value).then((r) => {
@@ -2266,8 +3277,8 @@ function AutocompleteInput({
2266
3277
  clearTimeout(t);
2267
3278
  };
2268
3279
  }, [api, field.name, value]);
2269
- return /* @__PURE__ */ jsxs5("span", { className: "qt-autocomplete", children: [
2270
- /* @__PURE__ */ jsx5(
3280
+ return /* @__PURE__ */ jsxs6("span", { className: "qt-autocomplete", children: [
3281
+ /* @__PURE__ */ jsx6(
2271
3282
  "input",
2272
3283
  {
2273
3284
  className: cx3("qt-chip-val", classNames?.input),
@@ -2279,8 +3290,8 @@ function AutocompleteInput({
2279
3290
  onChange: (e) => onChange(e.target.value)
2280
3291
  }
2281
3292
  ),
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" })
3293
+ /* @__PURE__ */ jsx6("datalist", { id: listId, children: result.values.map((v) => /* @__PURE__ */ jsx6("option", { value: v }, v)) }),
3294
+ result.hasMore && /* @__PURE__ */ jsx6("span", { className: "qt-autocomplete-hint", children: "keep typing to refine\u2026" })
2284
3295
  ] });
2285
3296
  }
2286
3297
  function OrderTermChip({
@@ -2297,6 +3308,7 @@ function OrderTermChip({
2297
3308
  onDragEnd,
2298
3309
  disabled,
2299
3310
  updateTerm,
3311
+ clearExtract,
2300
3312
  removeTerm,
2301
3313
  position,
2302
3314
  count,
@@ -2305,7 +3317,7 @@ function OrderTermChip({
2305
3317
  label
2306
3318
  }) {
2307
3319
  const fieldHasNull = useFieldHasNull(api, field?.name);
2308
- return /* @__PURE__ */ jsxs5(
3320
+ return /* @__PURE__ */ jsxs6(
2309
3321
  "span",
2310
3322
  {
2311
3323
  className: cx3("qt-chip", "qt-chip--col", "qt-chip--sort", isDragSource && "qt-chip--dragging", classNames?.chip),
@@ -2323,10 +3335,10 @@ function OrderTermChip({
2323
3335
  onDragEnd,
2324
3336
  title: "drag to reorder sort priority",
2325
3337
  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(
3338
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
3339
+ showPriority && /* @__PURE__ */ jsx6("span", { className: "qt-chip-priority", children: priority }),
3340
+ /* @__PURE__ */ jsx6("span", { className: "qt-chip-field", children: label }),
3341
+ /* @__PURE__ */ jsx6(
2330
3342
  "button",
2331
3343
  {
2332
3344
  type: "button",
@@ -2336,7 +3348,45 @@ function OrderTermChip({
2336
3348
  children: term.dir === "asc" ? "\u2191 asc" : "\u2193 desc"
2337
3349
  }
2338
3350
  ),
2339
- fieldHasNull !== false && /* @__PURE__ */ jsxs5(
3351
+ term.extract ? /* @__PURE__ */ jsxs6("span", { className: "qt-sort-extract", children: [
3352
+ /* @__PURE__ */ jsx6("span", { className: "qt-chip-and", children: "extract" }),
3353
+ /* @__PURE__ */ jsx6(
3354
+ "input",
3355
+ {
3356
+ className: cx3("qt-chip-val", classNames?.input),
3357
+ value: term.extract.regex,
3358
+ placeholder: "regex",
3359
+ "aria-label": `Regex extract for ${label}`,
3360
+ size: chipFieldWidthForInput(term.extract.regex || "regex", "text", 5, 4),
3361
+ style: { width: `${chipFieldWidthForInput(term.extract.regex || "regex", "text", 5, 4)}ch` },
3362
+ disabled,
3363
+ onChange: (e) => updateTerm(dataIndex, { extract: { regex: e.target.value } })
3364
+ }
3365
+ ),
3366
+ /* @__PURE__ */ jsx6(
3367
+ "button",
3368
+ {
3369
+ type: "button",
3370
+ className: "qt-chip-x",
3371
+ title: "Remove regex extraction",
3372
+ "aria-label": `Remove regex extract for ${label}`,
3373
+ disabled,
3374
+ onClick: () => clearExtract(dataIndex),
3375
+ children: "\u2715"
3376
+ }
3377
+ )
3378
+ ] }) : /* @__PURE__ */ jsx6(
3379
+ "button",
3380
+ {
3381
+ type: "button",
3382
+ className: "qt-chip-op-btn",
3383
+ title: "Sort by text extracted with a regular expression",
3384
+ disabled,
3385
+ onClick: () => updateTerm(dataIndex, { extract: { regex: "" } }),
3386
+ children: "+ regex extract"
3387
+ }
3388
+ ),
3389
+ (term.extract != null || fieldHasNull !== false) && /* @__PURE__ */ jsxs6(
2340
3390
  "button",
2341
3391
  {
2342
3392
  type: "button",
@@ -2350,7 +3400,7 @@ function OrderTermChip({
2350
3400
  ]
2351
3401
  }
2352
3402
  ),
2353
- /* @__PURE__ */ jsx5(
3403
+ /* @__PURE__ */ jsx6(
2354
3404
  MoveButtons,
2355
3405
  {
2356
3406
  label,
@@ -2360,7 +3410,7 @@ function OrderTermChip({
2360
3410
  onMove: moveTerm
2361
3411
  }
2362
3412
  ),
2363
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => removeTerm(dataIndex), disabled, children: "\u2715" })
3413
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-chip-x", onClick: () => removeTerm(dataIndex), disabled, children: "\u2715" })
2364
3414
  ]
2365
3415
  }
2366
3416
  );
@@ -2372,15 +3422,24 @@ function OrderRow({
2372
3422
  disabled
2373
3423
  }) {
2374
3424
  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);
3425
+ const byName = useMemo4(() => new Map(fields.map((f) => [f.name, f])), [fields]);
3426
+ const [drag, setDrag] = useState5(null);
3427
+ const [adding, setAdding] = useState5(false);
2378
3428
  function setOrderBy(next) {
2379
3429
  api.setSort(next);
2380
3430
  }
2381
3431
  function updateTerm(i, patch) {
2382
3432
  setOrderBy(orderBy.map((o, k) => k === i ? { ...o, ...patch } : o));
2383
3433
  }
3434
+ function clearExtract(i) {
3435
+ setOrderBy(
3436
+ orderBy.map((term, index) => {
3437
+ if (index !== i) return term;
3438
+ const { extract: _extract, ...plainTerm } = term;
3439
+ return plainTerm;
3440
+ })
3441
+ );
3442
+ }
2384
3443
  function removeTerm(i) {
2385
3444
  setOrderBy(orderBy.filter((_, k) => k !== i));
2386
3445
  }
@@ -2406,7 +3465,7 @@ function OrderRow({
2406
3465
  setAdding(false);
2407
3466
  }
2408
3467
  const termFields = orderBy.map((o) => o.field);
2409
- const termByField = useMemo3(() => new Map(orderBy.map((o) => [o.field, o])), [orderBy]);
3468
+ const termByField = useMemo4(() => new Map(orderBy.map((o) => [o.field, o])), [orderBy]);
2410
3469
  function previewFields() {
2411
3470
  if (!drag) return termFields;
2412
3471
  const without = termFields.filter((f) => f !== drag.source);
@@ -2436,14 +3495,14 @@ function OrderRow({
2436
3495
  const sortable = fields.filter(isSortable2);
2437
3496
  const isDefaultOrderBy = orderBy.length === api.defaults.orderBy.length && orderBy.every((term, i) => {
2438
3497
  const next = api.defaults.orderBy[i];
2439
- return term.field === next?.field && term.dir === next?.dir && (term.nulls ?? "last") === (next?.nulls ?? "last");
3498
+ return term.field === next?.field && term.dir === next?.dir && (term.nulls ?? "last") === (next?.nulls ?? "last") && (term.extract?.regex ?? null) === (next?.extract?.regex ?? null);
2440
3499
  });
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)" }),
3500
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-qb-row", children: [
3501
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "order by" }),
3502
+ orderBy.length === 0 && !adding && /* @__PURE__ */ jsx6("span", { className: "qt-qb-hint", children: "(default)" }),
2444
3503
  rendered.map((f, pos) => {
2445
3504
  const term = termByField.get(f);
2446
- return /* @__PURE__ */ jsx5(
3505
+ return /* @__PURE__ */ jsx6(
2447
3506
  OrderTermChip,
2448
3507
  {
2449
3508
  api,
@@ -2459,6 +3518,7 @@ function OrderRow({
2459
3518
  onDragEnd: () => setDrag(null),
2460
3519
  disabled,
2461
3520
  updateTerm,
3521
+ clearExtract,
2462
3522
  removeTerm,
2463
3523
  position: pos,
2464
3524
  count: rendered.length,
@@ -2476,8 +3536,8 @@ function OrderRow({
2476
3536
  f
2477
3537
  );
2478
3538
  }),
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" })
3539
+ adding ? /* @__PURE__ */ jsx6(FieldPicker, { fields: sortable, onPick: addTerm, onClose: () => setAdding(false) }) : /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add sort" }),
3540
+ !isDefaultOrderBy && /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-link-btn", onClick: () => api.setSort(api.defaults.orderBy), disabled, children: "reset" })
2481
3541
  ] });
2482
3542
  }
2483
3543
  function WindowRow({
@@ -2486,10 +3546,10 @@ function WindowRow({
2486
3546
  disabled
2487
3547
  }) {
2488
3548
  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]);
3549
+ const [limitDraft, setLimitDraft] = useState5(String(query.limit));
3550
+ const [offsetDraft, setOffsetDraft] = useState5(String(query.offset));
3551
+ useEffect5(() => setLimitDraft(String(query.limit)), [query.limit]);
3552
+ useEffect5(() => setOffsetDraft(String(query.offset)), [query.offset]);
2493
3553
  const hasLimitDefault = query.limit === api.defaults.limit;
2494
3554
  const hasOffsetDefault = query.offset === api.defaults.offset;
2495
3555
  const hasWindowDefault = hasLimitDefault && hasOffsetDefault;
@@ -2499,10 +3559,10 @@ function WindowRow({
2499
3559
  function commitOffset() {
2500
3560
  api.setOffset(Math.max(0, Number(offsetDraft) || 0));
2501
3561
  }
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(
3562
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-qb-row qt-qb-pagination", children: [
3563
+ /* @__PURE__ */ jsxs6("label", { className: "qt-qb-window-field", children: [
3564
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "limit" }),
3565
+ /* @__PURE__ */ jsx6(
2506
3566
  "input",
2507
3567
  {
2508
3568
  className: cx3("qt-qb-num", classNames?.input),
@@ -2516,9 +3576,9 @@ function WindowRow({
2516
3576
  }
2517
3577
  )
2518
3578
  ] }),
2519
- /* @__PURE__ */ jsxs5("label", { className: "qt-qb-window-field", children: [
2520
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "offset" }),
2521
- /* @__PURE__ */ jsx5(
3579
+ /* @__PURE__ */ jsxs6("label", { className: "qt-qb-window-field", children: [
3580
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "offset" }),
3581
+ /* @__PURE__ */ jsx6(
2522
3582
  "input",
2523
3583
  {
2524
3584
  className: cx3("qt-qb-num", classNames?.input),
@@ -2532,7 +3592,7 @@ function WindowRow({
2532
3592
  }
2533
3593
  )
2534
3594
  ] }),
2535
- !hasWindowDefault && /* @__PURE__ */ jsx5(
3595
+ !hasWindowDefault && /* @__PURE__ */ jsx6(
2536
3596
  "button",
2537
3597
  {
2538
3598
  type: "button",
@@ -2565,11 +3625,11 @@ function MetricsRow({
2565
3625
  }) {
2566
3626
  const { aggregations } = api;
2567
3627
  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);
3628
+ const measurable = useMemo4(() => fields.filter(isMeasurable), [fields]);
3629
+ const groupable = useMemo4(() => fields.filter(isGroupable), [fields]);
3630
+ const [drag, setDrag] = useState5(null);
2571
3631
  const ids = clauses.map((c) => c.id);
2572
- const byId = useMemo3(() => new Map(clauses.map((c) => [c.id, c])), [clauses]);
3632
+ const byId = useMemo4(() => new Map(clauses.map((c) => [c.id, c])), [clauses]);
2573
3633
  function previewIds() {
2574
3634
  if (!drag) return ids;
2575
3635
  const without = ids.filter((i) => i !== drag.source);
@@ -2595,12 +3655,12 @@ function MetricsRow({
2595
3655
  setDrag(null);
2596
3656
  }
2597
3657
  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" }),
3658
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-qb-row qt-qb-row--metrics", children: [
3659
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "metrics" }),
3660
+ clauses.length === 0 && /* @__PURE__ */ jsx6("span", { className: "qt-qb-hint", children: "none" }),
2601
3661
  rendered.map((id, position) => {
2602
3662
  const clause = byId.get(id);
2603
- return /* @__PURE__ */ jsx5(
3663
+ return /* @__PURE__ */ jsx6(
2604
3664
  MetricChip,
2605
3665
  {
2606
3666
  clause,
@@ -2625,8 +3685,8 @@ function MetricsRow({
2625
3685
  id
2626
3686
  );
2627
3687
  }),
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" })
3688
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-add", onClick: () => aggregations.add(), disabled, children: "+ add metric" }),
3689
+ clauses.length > 0 && /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-link-btn", onClick: aggregations.clear, disabled, title: "Remove all metrics", children: "reset" })
2630
3690
  ] });
2631
3691
  }
2632
3692
  function changeMetricOp(aggregations, clause, measurable, op) {
@@ -2655,11 +3715,11 @@ function MetricChip({
2655
3715
  count,
2656
3716
  onMove
2657
3717
  }) {
2658
- const [addingGroup, setAddingGroup] = useState4(false);
2659
- const byName = useMemo3(() => new Map(groupable.map((f) => [f.name, f])), [groupable]);
3718
+ const [addingGroup, setAddingGroup] = useState5(false);
3719
+ const byName = useMemo4(() => new Map(groupable.map((f) => [f.name, f])), [groupable]);
2660
3720
  const needsField = aggOpNeedsField(clause.op);
2661
3721
  const measureOptions = measurable.filter((f) => aggOpsForField(f).includes(clause.op));
2662
- return /* @__PURE__ */ jsxs5(
3722
+ return /* @__PURE__ */ jsxs6(
2663
3723
  "span",
2664
3724
  {
2665
3725
  className: cx3("qt-chip", "qt-chip--agg", isDragSource && "qt-chip--dragging", classNames?.chip),
@@ -2677,18 +3737,18 @@ function MetricChip({
2677
3737
  onDragEnd,
2678
3738
  title: "drag to reorder metrics",
2679
3739
  children: [
2680
- /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2681
- /* @__PURE__ */ jsx5(
3740
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
3741
+ /* @__PURE__ */ jsx6(
2682
3742
  "select",
2683
3743
  {
2684
3744
  className: cx3("qt-chip-op", classNames?.select),
2685
3745
  value: clause.op,
2686
3746
  disabled,
2687
3747
  onChange: (e) => onChangeOp(e.target.value),
2688
- children: AGG_OPS.map((op) => /* @__PURE__ */ jsx5("option", { value: op, children: AGG_OP_LABELS[op] }, op))
3748
+ children: AGG_OPS.map((op) => /* @__PURE__ */ jsx6("option", { value: op, children: AGG_OP_LABELS[op] }, op))
2689
3749
  }
2690
3750
  ),
2691
- /* @__PURE__ */ jsxs5(
3751
+ /* @__PURE__ */ jsxs6(
2692
3752
  "select",
2693
3753
  {
2694
3754
  className: cx3("qt-chip-val", classNames?.select),
@@ -2696,18 +3756,18 @@ function MetricChip({
2696
3756
  disabled,
2697
3757
  onChange: (e) => onChangeField(e.target.value || void 0),
2698
3758
  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))
3759
+ !needsField && /* @__PURE__ */ jsx6("option", { value: "", children: "(all rows)" }),
3760
+ needsField && clause.field == null && /* @__PURE__ */ jsx6("option", { value: "", children: "measure\u2026" }),
3761
+ measureOptions.map((f) => /* @__PURE__ */ jsx6("option", { value: f.name, children: f.label }, f.name))
2702
3762
  ]
2703
3763
  }
2704
3764
  ),
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" })
3765
+ /* @__PURE__ */ jsxs6("span", { className: "qt-chip-agg-by", children: [
3766
+ clause.groupBy.map((g) => /* @__PURE__ */ jsxs6("span", { className: "qt-chip-agg-group", children: [
3767
+ /* @__PURE__ */ jsx6("span", { className: "qt-chip-agg-group-label", children: byName.get(g)?.label ?? g }),
3768
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-chip-x", onClick: () => onRemoveGroup(g), disabled, children: "\u2715" })
2709
3769
  ] }, g)),
2710
- addingGroup ? /* @__PURE__ */ jsx5(
3770
+ addingGroup ? /* @__PURE__ */ jsx6(
2711
3771
  FieldPicker,
2712
3772
  {
2713
3773
  fields: groupable,
@@ -2718,7 +3778,7 @@ function MetricChip({
2718
3778
  },
2719
3779
  onClose: () => setAddingGroup(false)
2720
3780
  }
2721
- ) : /* @__PURE__ */ jsx5(
3781
+ ) : /* @__PURE__ */ jsx6(
2722
3782
  "button",
2723
3783
  {
2724
3784
  type: "button",
@@ -2730,7 +3790,7 @@ function MetricChip({
2730
3790
  }
2731
3791
  )
2732
3792
  ] }),
2733
- /* @__PURE__ */ jsx5(
3793
+ /* @__PURE__ */ jsx6(
2734
3794
  MoveButtons,
2735
3795
  {
2736
3796
  label: clause.label ?? `${AGG_OP_LABELS[clause.op]} metric`,
@@ -2740,16 +3800,16 @@ function MetricChip({
2740
3800
  onMove
2741
3801
  }
2742
3802
  ),
2743
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: onRemove, disabled, title: "Remove metric", children: "\u2715" })
3803
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-chip-x", onClick: onRemove, disabled, title: "Remove metric", children: "\u2715" })
2744
3804
  ]
2745
3805
  }
2746
3806
  );
2747
3807
  }
2748
3808
 
2749
3809
  // src/MetricsPanel.tsx
2750
- import { useMemo as useMemo4 } from "react";
3810
+ import { useMemo as useMemo5 } from "react";
2751
3811
  import { EMPTY_QUERY } from "@pythia-software/query-table-core";
2752
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3812
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2753
3813
  var cx4 = (...parts) => parts.filter((p) => Boolean(p)).join(" ");
2754
3814
  var OP_LABELS = {
2755
3815
  count: "Count",
@@ -2767,18 +3827,18 @@ function MetricsPanel({
2767
3827
  emptyMessage,
2768
3828
  classNames
2769
3829
  }) {
2770
- const byName = useMemo4(() => new Map(fields.map((f) => [f.name, f])), [fields]);
3830
+ const byName = useMemo5(() => new Map(fields.map((f) => [f.name, f])), [fields]);
2771
3831
  const { clauses, results, loading, error } = aggregations;
2772
3832
  if (clauses.length === 0) {
2773
- return emptyMessage ? /* @__PURE__ */ jsx6("div", { className: cx4("qt-metrics", classNames?.root), children: emptyMessage }) : null;
3833
+ return emptyMessage ? /* @__PURE__ */ jsx7("div", { className: cx4("qt-metrics", classNames?.root), children: emptyMessage }) : null;
2774
3834
  }
2775
3835
  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: [
3836
+ return /* @__PURE__ */ jsxs7("div", { className: cx4("qt-metrics", classNames?.root), children: [
3837
+ error ? /* @__PURE__ */ jsxs7("div", { className: "qt-metrics-error", children: [
2778
3838
  "Metrics failed: ",
2779
3839
  error.message
2780
3840
  ] }) : null,
2781
- clauses.map((clause) => /* @__PURE__ */ jsx6(
3841
+ clauses.map((clause) => /* @__PURE__ */ jsx7(
2782
3842
  MetricCard,
2783
3843
  {
2784
3844
  clause,
@@ -2802,23 +3862,23 @@ function MetricCard({
2802
3862
  }) {
2803
3863
  const measure = clause.field ? byName.get(clause.field) : void 0;
2804
3864
  const groupLabels = clause.groupBy.map((g) => byName.get(g)?.label ?? g);
2805
- const decimals = useMemo4(() => cardDecimals(buckets), [buckets]);
3865
+ const decimals = useMemo5(() => cardDecimals(buckets), [buckets]);
2806
3866
  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: [
3867
+ return /* @__PURE__ */ jsxs7("div", { className: cx4("qt-metric", `qt-metric--gb${Math.min(clause.groupBy.length, 3)}`, className), children: [
3868
+ /* @__PURE__ */ jsxs7("div", { className: "qt-metric-head", children: [
3869
+ /* @__PURE__ */ jsx7("span", { className: "qt-metric-title", children: metricTitle(clause, measure) }),
3870
+ groupLabels.length > 0 && /* @__PURE__ */ jsxs7("span", { className: "qt-metric-by", children: [
2811
3871
  "by ",
2812
3872
  groupLabels.join(" \xD7 ")
2813
3873
  ] })
2814
3874
  ] }),
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) })
3875
+ loading && buckets.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "qt-metric-loading", children: "\u2026" }) : buckets.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "qt-metric-empty", children: "no data" }) : clause.groupBy.length === 0 ? /* @__PURE__ */ jsx7(BigNumber, { bucket: buckets[0], fmt }) : clause.groupBy.length === 1 ? /* @__PURE__ */ jsx7(BarList, { buckets, fmt }) : clause.groupBy.length === 2 ? /* @__PURE__ */ jsx7(Pivot, { buckets, fmt, rowLabel: groupLabels[0], colLabel: groupLabels[1] }) : /* @__PURE__ */ jsx7(FlatTable, { buckets, fmt, groupLabels, valueLabel: metricTitle(clause, measure) })
2816
3876
  ] });
2817
3877
  }
2818
3878
  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: [
3879
+ return /* @__PURE__ */ jsxs7("div", { className: "qt-metric-big", children: [
3880
+ /* @__PURE__ */ jsx7("span", { className: "qt-metric-big-value", children: fmt(bucket.value) }),
3881
+ /* @__PURE__ */ jsxs7("span", { className: "qt-metric-big-sub", children: [
2822
3882
  bucket.count.toLocaleString(),
2823
3883
  " rows"
2824
3884
  ] })
@@ -2826,12 +3886,12 @@ function BigNumber({ bucket, fmt }) {
2826
3886
  }
2827
3887
  function BarList({ buckets, fmt }) {
2828
3888
  const max = Math.max(...buckets.map((b) => barMagnitude(b)), 0);
2829
- return /* @__PURE__ */ jsx6("div", { className: "qt-metric-bars", children: buckets.map((b, i) => {
3889
+ return /* @__PURE__ */ jsx7("div", { className: "qt-metric-bars", children: buckets.map((b, i) => {
2830
3890
  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) })
3891
+ return /* @__PURE__ */ jsxs7("div", { className: "qt-metric-bar-row", children: [
3892
+ /* @__PURE__ */ jsx7("span", { className: "qt-metric-bar-label", title: keyText(b.keys[0]), children: keyNode(b.keys[0]) }),
3893
+ /* @__PURE__ */ jsx7("span", { className: "qt-metric-bar-track", children: /* @__PURE__ */ jsx7("span", { className: "qt-metric-bar-fill", style: { width: `${pct}%` } }) }),
3894
+ /* @__PURE__ */ jsx7("span", { className: "qt-metric-bar-value", children: fmt(b.value) })
2835
3895
  ] }, `${keyText(b.keys[0])}-${i}`);
2836
3896
  }) });
2837
3897
  }
@@ -2845,22 +3905,22 @@ function Pivot({
2845
3905
  const colKeys = distinctKeys(buckets, 1);
2846
3906
  const cell = /* @__PURE__ */ new Map();
2847
3907
  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: [
3908
+ return /* @__PURE__ */ jsx7("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs7("table", { className: "qt-metric-pivot", children: [
3909
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs7("tr", { children: [
3910
+ /* @__PURE__ */ jsx7("th", { className: "qt-metric-pivot-corner", children: /* @__PURE__ */ jsxs7("span", { className: "qt-metric-pivot-axes", children: [
2851
3911
  rowLabel,
2852
3912
  " ",
2853
- /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\\" }),
3913
+ /* @__PURE__ */ jsx7("span", { className: "qt-muted", children: "\\" }),
2854
3914
  " ",
2855
3915
  colLabel
2856
3916
  ] }) }),
2857
- colKeys.map((c) => /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-col", children: keyNode(c) }, keyText(c)))
3917
+ colKeys.map((c) => /* @__PURE__ */ jsx7("th", { className: "qt-metric-pivot-col", children: keyNode(c) }, keyText(c)))
2858
3918
  ] }) }),
2859
- /* @__PURE__ */ jsx6("tbody", { children: rowKeys.map((r) => /* @__PURE__ */ jsxs6("tr", { children: [
2860
- /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-row", children: keyNode(r) }),
3919
+ /* @__PURE__ */ jsx7("tbody", { children: rowKeys.map((r) => /* @__PURE__ */ jsxs7("tr", { children: [
3920
+ /* @__PURE__ */ jsx7("th", { className: "qt-metric-pivot-row", children: keyNode(r) }),
2861
3921
  colKeys.map((c) => {
2862
3922
  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));
3923
+ return /* @__PURE__ */ jsx7("td", { className: "qt-metric-pivot-cell", children: v === void 0 ? /* @__PURE__ */ jsx7("span", { className: "qt-muted", children: "\xB7" }) : fmt(v) }, keyText(c));
2864
3924
  })
2865
3925
  ] }, keyText(r))) })
2866
3926
  ] }) });
@@ -2869,16 +3929,16 @@ function FlatTable({
2869
3929
  buckets,
2870
3930
  fmt,
2871
3931
  groupLabels,
2872
- valueLabel
3932
+ valueLabel: valueLabel2
2873
3933
  }) {
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 })
3934
+ return /* @__PURE__ */ jsx7("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs7("table", { className: "qt-metric-flat", children: [
3935
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs7("tr", { children: [
3936
+ groupLabels.map((g) => /* @__PURE__ */ jsx7("th", { children: g }, g)),
3937
+ /* @__PURE__ */ jsx7("th", { className: "qt-metric-flat-value", children: valueLabel2 })
2878
3938
  ] }) }),
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) })
3939
+ /* @__PURE__ */ jsx7("tbody", { children: buckets.map((b, i) => /* @__PURE__ */ jsxs7("tr", { children: [
3940
+ groupLabels.map((_, j) => /* @__PURE__ */ jsx7("td", { children: keyNode(b.keys[j]) }, j)),
3941
+ /* @__PURE__ */ jsx7("td", { className: "qt-metric-flat-value", children: fmt(b.value) })
2882
3942
  ] }, i)) })
2883
3943
  ] }) });
2884
3944
  }
@@ -2889,7 +3949,7 @@ function metricTitle(clause, measure) {
2889
3949
  }
2890
3950
  var UNIT_RENDER_KEYS = /* @__PURE__ */ new Set(["duration_ms", "byte_size"]);
2891
3951
  function formatMetricValue(value, op, measure, renderers, decimals) {
2892
- if (value == null) return /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\u2014" });
3952
+ if (value == null) return /* @__PURE__ */ jsx7("span", { className: "qt-muted", children: "\u2014" });
2893
3953
  if (op === "count" || op === "count_distinct") {
2894
3954
  return typeof value === "number" ? formatNumber(value, 0) : String(value);
2895
3955
  }
@@ -2952,21 +4012,21 @@ function keyText(k) {
2952
4012
  return k == null ? NULL_KEY : String(k);
2953
4013
  }
2954
4014
  function keyNode(k) {
2955
- return k == null ? /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: NULL_KEY }) : String(k);
4015
+ return k == null ? /* @__PURE__ */ jsx7("span", { className: "qt-muted", children: NULL_KEY }) : String(k);
2956
4016
  }
2957
4017
 
2958
4018
  // src/SelectionToolbar.tsx
2959
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
4019
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2960
4020
  var cx5 = (...parts) => parts.filter(Boolean).join(" ");
2961
4021
  function SelectionToolbar({ selection, actions, alwaysShow, className }) {
2962
4022
  if (selection.count === 0 && !alwaysShow) return null;
2963
4023
  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: [
4024
+ return /* @__PURE__ */ jsxs8("div", { className: cx5("qt-selection-toolbar", className), role: "toolbar", children: [
4025
+ /* @__PURE__ */ jsxs8("span", { className: "qt-selection-count", children: [
2966
4026
  selection.count,
2967
4027
  " selected"
2968
4028
  ] }),
2969
- /* @__PURE__ */ jsx7(
4029
+ /* @__PURE__ */ jsx8(
2970
4030
  "button",
2971
4031
  {
2972
4032
  type: "button",
@@ -2976,7 +4036,7 @@ function SelectionToolbar({ selection, actions, alwaysShow, className }) {
2976
4036
  children: "clear"
2977
4037
  }
2978
4038
  ),
2979
- /* @__PURE__ */ jsx7("span", { className: "qt-selection-actions", children: actions(selectedIds) })
4039
+ /* @__PURE__ */ jsx8("span", { className: "qt-selection-actions", children: actions(selectedIds, selection) })
2980
4040
  ] });
2981
4041
  }
2982
4042
  export {
@@ -2986,6 +4046,7 @@ export {
2986
4046
  MetricsPanel,
2987
4047
  QueryBuilder,
2988
4048
  SavedQueriesModal,
4049
+ SelectColumnEditor,
2989
4050
  SelectionToolbar,
2990
4051
  defaultRenderers,
2991
4052
  resolveRenderer,