@pythia-software/query-table-ui 0.9.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,8 +29,8 @@ and selection toolbar.
29
29
  for complete local datasets. The standard query builder supplies GROUP BY,
30
30
  HAVING, aggregate column editing and representative-row ordering. Use
31
31
  `api.schema`, `api.visibleFields` and `api.rowId` for the active result shape;
32
- set `grouping: false` to opt out. Backend transports currently reject grouped
33
- row queries. See [grouping integration](../../docs/grouping.md) for the query
32
+ set `grouping: false` to opt out. Remote tables group on the server when its
33
+ `queryCapabilities.groupedRows` is true. See [grouping integration](../../docs/grouping.md) for the query
34
34
  shape, source-stage controls, saved state and execution boundaries.
35
35
 
36
36
  ## Install
@@ -110,24 +110,41 @@ Click anywhere in a column header to open its actions menu, including padding
110
110
  and blank space. Resize handles remain dedicated to resizing.
111
111
  Open **Column Widths** to resize in one step:
112
112
 
113
- - **Fit This Column** sizes the clicked column to its widest rendered value.
114
- - **Fit All to Content** does the same for every column.
115
- - **Fit All to Screen** fills the viewport; when content overflows, narrow
116
- columns keep their content width and only the widest ones are trimmed. It
117
- sets `query.fitToScreen`, so shared links and saved queries fit each viewer's
118
- available table area after loading and when that area changes. No column
119
- is trimmed below what its buttons and links need (links shorten to an
120
- ellipsis first); if those minimums exceed the screen, the table scrolls
121
- horizontally. Set `select.minWidth` on a field whose host renderer needs
122
- room the table cannot measure.
123
- - **Equal Widths** splits the viewport evenly.
124
- - **Reset Widths** drops explicit widths back to schema defaults.
125
-
126
- Widths live in `query.select`, so presets are shareable and undoable.
127
- The fit-to-screen preference travels in the existing encoded `?q=` parameter
128
- (`fs: true` in its compact payload). Manual column resizing, fitting a single
129
- column, another width preset, or resetting widths clears it. Set
130
- `fitToScreen: true` on an initial query to enable the same behavior directly.
113
+ - **Fit All to Content** sets `query.fit = "content"` and clears explicit
114
+ widths. Every column without a width then keeps fitting its widest drawn
115
+ value (header included) after each change to what the table shows: sorting,
116
+ column changes, filters, grouping, paging, refreshes and new rows. The table
117
+ is then only as wide as its columns.
118
+ - **Fit All to Screen** sets `query.fit = "screen"` and clears explicit
119
+ widths. Columns without a width fill the viewport left by resized ones;
120
+ when content overflows, narrow columns keep their content width and only
121
+ the widest ones are trimmed. It also refits when the viewport changes.
122
+ - **Fit This Column** returns the clicked column to automatic fitting in
123
+ either mode. With no mode it sizes the column to its widest drawn value once.
124
+ - **Equal Widths** splits the viewport evenly and turns any mode off.
125
+ - **Reset Widths** drops explicit widths back to automatic fitting (or to
126
+ schema defaults with no mode).
127
+
128
+ Choosing the active mode again turns it off and keeps the widths it showed.
129
+
130
+ Fitted widths are local layout: they are never written to the query, the URL
131
+ or undo history. Dragging or nudging one column stores only that column's
132
+ `width`; the mode stays on and the other columns keep fitting around it. A
133
+ field's `select.width` is a fallback used only when no mode is set. While a
134
+ query is loading the previous widths stay; scrolling a virtualized body never
135
+ refits. A refresh that keeps the query (same columns, filters, sort, paging
136
+ and grouping) only grows content-fitted columns, so a live table stays calm;
137
+ any change to the query refits from scratch. Content fitting stops at 800px.
138
+ No column is fitted below what its buttons and links need (links shorten to
139
+ an ellipsis first); in screen mode, if those minimums exceed the screen, the
140
+ table scrolls horizontally. Set `select.minWidth` on a field whose host renderer needs room
141
+ the table cannot measure.
142
+
143
+ Set `fit` on a default or initial query to opt in; a stored query without
144
+ `fit` has no live sizing. The mode travels in the encoded `?q=` parameter
145
+ (`ft` in its compact payload). Links from 0.9.9 and earlier with
146
+ `fitToScreen: true` (`fs`) open in screen mode without their stored widths,
147
+ which screen fitting never showed.
131
148
 
132
149
  ## Cell quick filters
133
150
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, SetStateAction, CSSProperties, ButtonHTMLAttributes, PointerEvent, Ref, MutableRefObject } from 'react';
3
3
  import { FieldDef, QueryState, LinkTargets, CellDisplay, JsonCellDisplay, JsonInspectionState, JsonInspectionScope, AttentionRegistry, RowId, AggregationClause, AggregationBucket, AggregationResultEntry, MetricValue, SavedQuery, FieldStats, WhereClause, MetricValueFormat, FieldSchema, ScopedJsonInspectionState, PropertyViewState, RelationshipAdapter, ObjectReference, PropertyFormat, PropertyViewStorage, ItemPropertyMode, FormulaFunction, GroupedRow } from '@pythia-software/query-table-core';
4
- import { ItemDetailsApi, SelectionApi, ColumnDragApi, QueryTableApi, QueryFilterApi, AggregationsApi, SavedQueriesApi, RequestActivityApi, PropertyViewApi, GroupingApi } from '@pythia-software/query-table-react';
4
+ import { ItemDetailsApi, SelectionApi, ColumnDragApi, GroupingApi, QueryTableApi, QueryFilterApi, AggregationsApi, SavedQueriesApi, RequestActivityApi, PropertyViewApi } from '@pythia-software/query-table-react';
5
5
  import { Placement } from '@floating-ui/react';
6
6
 
7
7
  interface CellContext<Row = any, V = unknown> {
@@ -200,6 +200,9 @@ interface DataTableProps<Row> {
200
200
  * table header also live-previews/dims the matching QueryBuilder select chip
201
201
  * (and vice versa). Omit for a self-contained, table-only drag. */
202
202
  columnDrag?: ColumnDragApi;
203
+ /** Grouping controller (`api.grouping`). Pass it to offer Group By, Remove
204
+ * from Group By and Ungroup in the header menu. */
205
+ grouping?: GroupingApi<Row> | undefined;
203
206
  /** Always-visible trailing column (row actions, links). */
204
207
  trailing?: (row: Row) => ReactNode;
205
208
  trailingLabel?: string;
package/dist/index.js CHANGED
@@ -4202,7 +4202,7 @@ function ItemHoverCard({ api, query, id, renderers, anchor, onClose }) {
4202
4202
  }
4203
4203
 
4204
4204
  // src/DataTable.tsx
4205
- import { GROUP_ID_FIELD, GROUP_LINK_RENDER, ITEM_DETAILS_COLUMN, groupKeyFields, isComputedCellError, isGroupLinkValue as isGroupLinkValue2 } from "@pythia-software/query-table-core";
4205
+ import { GROUP_ID_FIELD, GROUP_LINK_RENDER, ITEM_DETAILS_COLUMN, groupKeyField, groupKeyFields, isComputedCellError, isGroupLinkValue as isGroupLinkValue2 } from "@pythia-software/query-table-core";
4206
4206
  import { useId as useId4, useCallback as useCallback3, useEffect as useEffect12, useLayoutEffect as useLayoutEffect6, useMemo as useMemo5, useRef as useRef14, useState as useState16 } from "react";
4207
4207
  import { defaultRangeExtractor, useVirtualizer } from "@tanstack/react-virtual";
4208
4208
  import { isSortable, readFieldValue as readFieldValue2 } from "@pythia-software/query-table-core";
@@ -4926,8 +4926,11 @@ function DataTable(props) {
4926
4926
  const [copiedCellKey, setCopiedCellKey] = useState16(null);
4927
4927
  const [headerHeight, setHeaderHeight] = useState16(0);
4928
4928
  const [showHorizontalCue, setShowHorizontalCue] = useState16(false);
4929
- const [screenWidths, setScreenWidths] = useState16(/* @__PURE__ */ new Map());
4929
+ const [fittedWidths, setFittedWidths] = useState16(/* @__PURE__ */ new Map());
4930
+ const lastFitRef = useRef14(null);
4930
4931
  const [screenViewportWidth, setScreenViewportWidth] = useState16(0);
4932
+ const [shownRevision, setShownRevision] = useState16(0);
4933
+ const fitWaitingRef = useRef14(false);
4931
4934
  const tableWrapRef = useRef14(null);
4932
4935
  const tableHeadRef = useRef14(null);
4933
4936
  const tableRef = useRef14(null);
@@ -5072,12 +5075,36 @@ function DataTable(props) {
5072
5075
  const term = query.orderBy[idx];
5073
5076
  return { dir: term.dir, priority: query.orderBy.length > 1 ? idx + 1 : null };
5074
5077
  }
5078
+ const grouping = props.grouping;
5079
+ function headerGrouping(f) {
5080
+ if (!grouping) return null;
5081
+ if (!query.grouping) {
5082
+ const groupable = grouping.supported && grouping.model.groupableFields.some((g) => g.name === f.name);
5083
+ return { kind: "rows", groupable };
5084
+ }
5085
+ return { kind: "groups", key: groupKeys ? groupKeys.includes(f.name) : false, keyCount: query.grouping.keys.length };
5086
+ }
5087
+ function changeGroupKeys(keys) {
5088
+ setMenu(null);
5089
+ if (!grouping) return;
5090
+ try {
5091
+ grouping.changeKeys(keys);
5092
+ } catch (error) {
5093
+ grouping.setNotice(error instanceof Error ? error.message : String(error));
5094
+ }
5095
+ }
5096
+ function removeGroupKey(f) {
5097
+ changeGroupKeys((query.grouping?.keys ?? []).filter((key) => groupKeyField(key) !== f.name));
5098
+ }
5075
5099
  function widthFor(name, f) {
5076
5100
  const col = query.select.find((c) => c.field === name);
5077
5101
  return col?.width ?? f.select?.width;
5078
5102
  }
5103
+ function explicitWidthFor(name) {
5104
+ return query.select.find((c) => c.field === name)?.width;
5105
+ }
5079
5106
  function resolvedWidthFor(name, f) {
5080
- if (query.fitToScreen && screenWidths.has(name)) return screenWidths.get(name);
5107
+ if (query.fit && explicitWidthFor(name) == null && fittedWidths.has(name)) return fittedWidths.get(name);
5081
5108
  return widthFor(name, f);
5082
5109
  }
5083
5110
  function resolvedColumnWidthFor(name, f) {
@@ -5098,7 +5125,6 @@ function DataTable(props) {
5098
5125
  function commitColumnWidth(name, width) {
5099
5126
  if (name === SELECTION_COLUMN) {
5100
5127
  setSelectionColumnWidth(clampColumnWidth(name, width));
5101
- if (query.fitToScreen) setWidths(/* @__PURE__ */ new Map());
5102
5128
  return;
5103
5129
  }
5104
5130
  setWidth(name, width);
@@ -5106,17 +5132,25 @@ function DataTable(props) {
5106
5132
  function setWidth(name, width) {
5107
5133
  setWidths(/* @__PURE__ */ new Map([[name, width]]));
5108
5134
  }
5109
- function setWidths(widths, fitToScreen = false) {
5135
+ function setWidths(widths, leaveFit = false) {
5110
5136
  const px = new Map(Array.from(widths, ([name, width]) => [name, clampWidth(width)]));
5111
5137
  onQueryChange((prev) => {
5112
- const fittedWidths = new Map([...screenWidths].filter(([name]) => fieldByName.has(name)));
5113
- const current = prev.fitToScreen ? writeWidths(prev.select, fields, fittedWidths) : prev.select;
5138
+ let current = prev.select;
5139
+ if (leaveFit && prev.fit) {
5140
+ const frozen = new Map([...fittedWidths].filter(([name]) => fieldByName.has(name) && prev.select.find((c) => c.field === name)?.width == null));
5141
+ current = writeWidths(current, fields, frozen);
5142
+ }
5114
5143
  const next = { ...prev, select: writeWidths(current, fields, px) };
5115
- if (fitToScreen) next.fitToScreen = true;
5116
- else delete next.fitToScreen;
5144
+ if (leaveFit) delete next.fit;
5117
5145
  return next;
5118
5146
  });
5119
5147
  }
5148
+ function clearWidths(names) {
5149
+ onQueryChange((prev) => ({
5150
+ ...prev,
5151
+ select: prev.select.map(({ width, ...c }) => names && !names.has(c.field) && width != null ? { ...c, width } : c)
5152
+ }));
5153
+ }
5120
5154
  function applyResizeGuide() {
5121
5155
  const preview = resizePreviewRef.current;
5122
5156
  const guide = resizeGuideRef.current;
@@ -5225,28 +5259,32 @@ function DataTable(props) {
5225
5259
  });
5226
5260
  }
5227
5261
  function autofitColumn(name) {
5262
+ if (query.fit) {
5263
+ if (explicitWidthFor(name) != null) clearWidths(/* @__PURE__ */ new Set([name]));
5264
+ return;
5265
+ }
5228
5266
  const [width = 0] = measureContentWidths([name]);
5229
5267
  if (width > 0) setWidth(name, width);
5230
5268
  }
5231
5269
  function applyWidthPreset(preset) {
5232
5270
  setMenu(null);
5271
+ if (preset !== "equal") {
5272
+ if (query.fit === preset) setWidths(/* @__PURE__ */ new Map(), true);
5273
+ else onQueryChange((prev) => ({ ...prev, fit: preset, select: prev.select.map(({ width: _width, ...c }) => c) }));
5274
+ return;
5275
+ }
5233
5276
  const names = renderedColumnNames.filter((name) => fieldByName.has(name));
5234
5277
  if (names.length === 0) return;
5235
- const content = measureContentWidths(names).map((w, i) => w || displayColumnWidthFor(names[i], fieldByName.get(names[i])));
5236
5278
  const wrap = tableWrapRef.current;
5237
5279
  const available = (wrap?.clientWidth ?? 0) - (showSel ? selectionColumnWidth : 0) - (trailing ? TRAILING_WIDTH : 0) - TABLE_BORDER_PX;
5238
- const widths = preset === "content" ? fitContentWidths(content) : preset === "equal" ? equalWidths(names.length, available) : fitScreenWidths(content, available, void 0, measureControlMinimums(names));
5239
- setWidths(new Map(names.map((name, i) => [name, widths[i]])), preset === "screen");
5280
+ const widths = equalWidths(names.length, available);
5281
+ setWidths(new Map(names.map((name, i) => [name, widths[i]])), true);
5240
5282
  }
5241
- const hasCustomWidths = query.fitToScreen === true || selectionColumnWidth !== DEFAULT_SELECTION_WIDTH || query.select.some((c) => c.width != null);
5283
+ const hasCustomWidths = selectionColumnWidth !== DEFAULT_SELECTION_WIDTH || query.select.some((c) => c.width != null);
5242
5284
  function resetWidths() {
5243
5285
  setMenu(null);
5244
5286
  setSelectionColumnWidth(DEFAULT_SELECTION_WIDTH);
5245
- onQueryChange((prev) => {
5246
- const next = { ...prev, select: prev.select.map(({ width: _width, ...c }) => c) };
5247
- delete next.fitToScreen;
5248
- return next;
5249
- });
5287
+ clearWidths();
5250
5288
  }
5251
5289
  function resizeWithKeyboard(name, startWidth, e) {
5252
5290
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight" && e.key !== "Enter") return;
@@ -5299,11 +5337,15 @@ function DataTable(props) {
5299
5337
  const index = preview.indexOf(SELECTION_COLUMN);
5300
5338
  if (index >= 0) setSelectionColumnIndex(index);
5301
5339
  }, [columnDrag.active, columnDrag.dropRevision, renderedColumnNames, fieldNames]);
5340
+ const fit = query.fit;
5302
5341
  useEffect12(() => {
5303
- if (!query.fitToScreen) return;
5342
+ if (!fit) return;
5304
5343
  const wrap = tableWrapRef.current;
5305
5344
  if (!wrap) return;
5306
- const measureViewport = () => setScreenViewportWidth(wrap.clientWidth);
5345
+ const measureViewport = () => {
5346
+ if (fit === "screen") setScreenViewportWidth(wrap.clientWidth);
5347
+ else if (fitWaitingRef.current && wrap.clientWidth > 0) setShownRevision((revision) => revision + 1);
5348
+ };
5307
5349
  measureViewport();
5308
5350
  window.addEventListener("resize", measureViewport);
5309
5351
  const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measureViewport);
@@ -5312,20 +5354,50 @@ function DataTable(props) {
5312
5354
  window.removeEventListener("resize", measureViewport);
5313
5355
  observer?.disconnect();
5314
5356
  };
5315
- }, [query.fitToScreen]);
5357
+ }, [fit]);
5358
+ const fitQueryKey = useMemo5(() => JSON.stringify([
5359
+ query.select.map((c) => [c.field, c.display ?? null]),
5360
+ query.where,
5361
+ query.orderBy,
5362
+ query.limit,
5363
+ query.offset,
5364
+ query.grouping ?? null,
5365
+ query.hostState ?? null,
5366
+ fieldNames
5367
+ ]), [query.select, query.where, query.orderBy, query.limit, query.offset, query.grouping, query.hostState, fieldNames]);
5316
5368
  useCommitLayoutEffect(() => {
5317
- if (!query.fitToScreen || loading || fieldNames.length === 0) return;
5318
- const available = (tableWrapRef.current?.clientWidth ?? 0) - (showSel ? selectionColumnWidth : 0) - (trailing ? TRAILING_WIDTH : 0) - TABLE_BORDER_PX;
5319
- if (available <= 0) return;
5320
- const content = measureContentWidths(fieldNames).map(
5321
- (width, i) => width || widthFor(fieldNames[i], fields[i]) || DEFAULT_WIDTH
5369
+ if (!fit) {
5370
+ lastFitRef.current = null;
5371
+ return;
5372
+ }
5373
+ if (loading || fieldNames.length === 0) return;
5374
+ const viewport = tableWrapRef.current?.clientWidth ?? 0;
5375
+ fitWaitingRef.current = viewport <= 0;
5376
+ if (viewport <= 0) return;
5377
+ const names = fieldNames.filter((name) => explicitWidthFor(name) == null);
5378
+ const content = measureContentWidths(names).map(
5379
+ (width, i) => width || widthFor(names[i], fieldByName.get(names[i])) || DEFAULT_WIDTH
5322
5380
  );
5323
- const widths = fitScreenWidths(content, available, void 0, measureControlMinimums(fieldNames));
5324
- setScreenWidths((current) => {
5325
- if (current.size === fieldNames.length && fieldNames.every((name, i) => current.get(name) === widths[i])) return current;
5326
- return new Map(fieldNames.map((name, i) => [name, widths[i]]));
5381
+ const minimums = measureControlMinimums(names);
5382
+ let widths;
5383
+ if (fit === "screen") {
5384
+ const fixed = fieldNames.reduce((sum, name) => sum + (explicitWidthFor(name) ?? 0), 0);
5385
+ const available = viewport - (showSel ? selectionColumnWidth : 0) - (trailing ? TRAILING_WIDTH : 0) - TABLE_BORDER_PX - fixed;
5386
+ widths = fitScreenWidths(content, available, void 0, minimums);
5387
+ } else {
5388
+ const last = lastFitRef.current;
5389
+ const grow = last?.fit === "content" && last.key === fitQueryKey;
5390
+ widths = fitContentWidths(content).map((width, i) => {
5391
+ const fitted = Math.min(MAX_WIDTH, Math.max(width, Math.ceil(minimums[i])));
5392
+ return grow ? Math.max(fitted, fittedWidths.get(names[i]) ?? 0) : fitted;
5393
+ });
5394
+ }
5395
+ lastFitRef.current = { fit, key: fitQueryKey };
5396
+ setFittedWidths((current) => {
5397
+ if (current.size === names.length && names.every((name, i) => current.get(name) === widths[i])) return current;
5398
+ return new Map(names.map((name, i) => [name, widths[i]]));
5327
5399
  });
5328
- }, [query.fitToScreen, query.select, fields, fieldNames, rows, loading, renderers, screenViewportWidth, showSel, selectionColumnWidth, trailing]);
5400
+ }, [fit, query.select, fitQueryKey, fields, fieldNames, rows, loading, renderers, screenViewportWidth, shownRevision, showSel, selectionColumnWidth, trailing]);
5329
5401
  function toggleHeader() {
5330
5402
  if (!selection) return;
5331
5403
  selection.setPage(pageIds, headerState !== "all");
@@ -5451,7 +5523,7 @@ function DataTable(props) {
5451
5523
  "aria-hidden": "true"
5452
5524
  }
5453
5525
  ),
5454
- /* @__PURE__ */ jsxs17("table", { ref: tableRef, className: cx3("qt-table", classNames?.table), style: { minWidth: tableWidth }, children: [
5526
+ /* @__PURE__ */ jsxs17("table", { ref: tableRef, className: cx3("qt-table", classNames?.table), style: fit === "content" ? { width: tableWidth, minWidth: tableWidth } : { minWidth: tableWidth }, children: [
5455
5527
  /* @__PURE__ */ jsxs17("colgroup", { children: [
5456
5528
  renderedColumnNames.map((name) => {
5457
5529
  const f = fieldByName.get(name);
@@ -5746,8 +5818,12 @@ function DataTable(props) {
5746
5818
  canRemoveColumn: fields.length > 1,
5747
5819
  onSetSort: (placement, dir) => applyHeaderSort(menu.field, dir, placement),
5748
5820
  onRemove: () => removeColumn(menu.field),
5821
+ grouping: headerGrouping(menu.field),
5822
+ onGroupBy: () => changeGroupKeys([menu.field.name]),
5823
+ onRemoveGroupKey: () => removeGroupKey(menu.field),
5824
+ onUngroup: () => changeGroupKeys([]),
5749
5825
  canResetWidths: hasCustomWidths,
5750
- fitToScreen: query.fitToScreen === true,
5826
+ fit: query.fit,
5751
5827
  onFitColumn: () => {
5752
5828
  setMenu(null);
5753
5829
  autofitColumn(menu.field.name);
@@ -5766,9 +5842,13 @@ function HeaderMenu({
5766
5842
  sortable,
5767
5843
  canRemoveColumn,
5768
5844
  canResetWidths,
5769
- fitToScreen,
5845
+ fit,
5770
5846
  onSetSort,
5771
5847
  onRemove,
5848
+ grouping,
5849
+ onGroupBy,
5850
+ onRemoveGroupKey,
5851
+ onUngroup,
5772
5852
  onFitColumn,
5773
5853
  onWidthPreset,
5774
5854
  onResetWidths,
@@ -5784,21 +5864,37 @@ function HeaderMenu({
5784
5864
  ")"
5785
5865
  ] }, `${placement}-${dir}`))
5786
5866
  ),
5867
+ grouping && /* @__PURE__ */ jsx21(MenuSeparator, {}),
5868
+ grouping?.kind === "rows" && /* @__PURE__ */ jsxs17(MenuItem, { disabled: !grouping.groupable, onSelect: onGroupBy, title: grouping.groupable ? `One row per ${field.label} value` : `${field.label} cannot be grouped`, children: [
5869
+ "Group By ",
5870
+ field.label
5871
+ ] }),
5872
+ grouping?.kind === "groups" && grouping.key && grouping.keyCount > 1 && /* @__PURE__ */ jsx21(MenuItem, { onSelect: onRemoveGroupKey, title: "Group by the remaining keys only", children: "Remove from Group By" }),
5873
+ grouping?.kind === "groups" && /* @__PURE__ */ jsx21(MenuItem, { onSelect: onUngroup, title: "Back to the source rows", children: "Ungroup" }),
5787
5874
  /* @__PURE__ */ jsx21(MenuSeparator, {}),
5788
5875
  /* @__PURE__ */ jsxs17(MenuSubmenu, { label: "Column Widths", children: [
5789
- /* @__PURE__ */ jsx21(MenuItem, { onSelect: onFitColumn, title: `Size ${field.label} to its widest visible value`, children: "Fit This Column" }),
5876
+ /* @__PURE__ */ jsx21(MenuItem, { onSelect: onFitColumn, title: fit ? `Return ${field.label} to automatic fitting` : `Size ${field.label} to its widest visible value`, children: "Fit This Column" }),
5790
5877
  /* @__PURE__ */ jsx21(MenuSeparator, {}),
5791
- WIDTH_PRESETS.map(({ preset, label, hint }) => /* @__PURE__ */ jsx21(MenuItem, { onSelect: () => onWidthPreset(preset), title: hint, checked: preset === "screen" ? fitToScreen : void 0, children: label }, preset)),
5878
+ WIDTH_PRESETS.map(({ preset, label, hint }) => /* @__PURE__ */ jsx21(
5879
+ MenuItem,
5880
+ {
5881
+ onSelect: () => onWidthPreset(preset),
5882
+ title: preset === fit ? "Stop fitting automatically and keep the current widths" : hint,
5883
+ checked: preset === "equal" ? void 0 : preset === fit,
5884
+ children: label
5885
+ },
5886
+ preset
5887
+ )),
5792
5888
  /* @__PURE__ */ jsx21(MenuSeparator, {}),
5793
- /* @__PURE__ */ jsx21(MenuItem, { disabled: !canResetWidths, onSelect: onResetWidths, title: "Back to the default column widths", children: "Reset Widths" })
5889
+ /* @__PURE__ */ jsx21(MenuItem, { disabled: !canResetWidths, onSelect: onResetWidths, title: fit ? "Return every resized column to automatic fitting" : "Back to the default column widths", children: "Reset Widths" })
5794
5890
  ] }),
5795
5891
  /* @__PURE__ */ jsx21(MenuSeparator, {}),
5796
5892
  /* @__PURE__ */ jsx21(MenuItem, { tone: "danger", disabled: !canRemoveColumn, onSelect: onRemove, children: "Remove Column" })
5797
5893
  ] });
5798
5894
  }
5799
5895
  var WIDTH_PRESETS = [
5800
- { preset: "content", label: "Fit All to Content", hint: "Size every column to its widest visible value" },
5801
- { preset: "screen", label: "Fit All to Screen", hint: "Fill the table width, trimming the widest columns first" },
5896
+ { preset: "content", label: "Fit All to Content", hint: "Keep every column sized to its widest visible value" },
5897
+ { preset: "screen", label: "Fit All to Screen", hint: "Keep the columns filling the table width, trimming the widest first" },
5802
5898
  { preset: "equal", label: "Equal Widths", hint: "Split the table width evenly across columns" }
5803
5899
  ];
5804
5900
  function nextHeaderOrderBy(existing, field, dir, placement) {
@@ -5891,7 +5987,7 @@ function valueToString2(value) {
5891
5987
  import { useState as useState21 } from "react";
5892
5988
  import {
5893
5989
  GROUP_KEY_INTERVALS,
5894
- groupKeyField,
5990
+ groupKeyField as groupKeyField2,
5895
5991
  isCalendarGroupKey,
5896
5992
  MAX_GROUP_BY_FIELDS,
5897
5993
  OP_LABEL as OP_LABEL4
@@ -8417,7 +8513,7 @@ function KeyIntervalControl({
8417
8513
  label,
8418
8514
  onChange
8419
8515
  }) {
8420
- const field = groupKeyField(value);
8516
+ const field = groupKeyField2(value);
8421
8517
  const interval = isCalendarGroupKey(value) ? value.interval : "";
8422
8518
  return /* @__PURE__ */ jsxs23(
8423
8519
  "span",
@@ -8508,7 +8604,7 @@ function GroupingClauses({
8508
8604
  /* @__PURE__ */ jsxs23("div", { className: "qt-qb-row", children: [
8509
8605
  /* @__PURE__ */ jsx27("div", { className: "qt-qb-section-header", children: /* @__PURE__ */ jsx27("span", { className: "qt-qb-kw", "data-mobile-label": "Group by", children: "group by" }) }),
8510
8606
  groupKeys.map((groupKey, index) => {
8511
- const key = groupKeyField(groupKey);
8607
+ const key = groupKeyField2(groupKey);
8512
8608
  const label = model.sourceField(key)?.label ?? key;
8513
8609
  return /* @__PURE__ */ jsxs23("span", { className: "qt-chip", children: [
8514
8610
  /* @__PURE__ */ jsx27(
@@ -8669,7 +8765,7 @@ function useGroupingPresentation(api, enabled = true) {
8669
8765
  "\xB7 GROUP BY",
8670
8766
  " ",
8671
8767
  /* @__PURE__ */ jsx27("strong", { children: groupKeys.map((key) => {
8672
- const name = groupKeyField(key);
8768
+ const name = groupKeyField2(key);
8673
8769
  const label = model.sourceField(name)?.label ?? name;
8674
8770
  return isCalendarGroupKey(key) ? `${label} (${key.interval})` : label;
8675
8771
  }).join(" + ") }),
@@ -13907,10 +14003,11 @@ function SetFilterEditor({ api, field, returnFocus, classNames, mode: initialMod
13907
14003
  useEffect20(() => {
13908
14004
  if (strategy.source !== "autocomplete") return;
13909
14005
  let cancelled = false;
14006
+ const ac = new AbortController();
13910
14007
  setLoading(true);
13911
14008
  setError("");
13912
14009
  const timer = setTimeout(() => {
13913
- api.filterValues(field.name, search).then((result) => {
14010
+ api.filterValues(field.name, search, ac.signal).then((result) => {
13914
14011
  if (!cancelled) setSuggestions(result.values);
13915
14012
  }).catch(() => {
13916
14013
  if (!cancelled) setError("Could not load values. Try searching again.");
@@ -13921,6 +14018,7 @@ function SetFilterEditor({ api, field, returnFocus, classNames, mode: initialMod
13921
14018
  return () => {
13922
14019
  cancelled = true;
13923
14020
  clearTimeout(timer);
14021
+ ac.abort();
13924
14022
  };
13925
14023
  }, [api.filterValues, field.name, strategy.source, search]);
13926
14024
  function toggle(value) {
@@ -14134,8 +14232,9 @@ function FilterValueAutocomplete({ api, field, value, contextKey, className, wid
14134
14232
  useEffect21(() => {
14135
14233
  if (!open || loaded) return;
14136
14234
  let cancelled = false;
14235
+ const ac = new AbortController();
14137
14236
  const timer = setTimeout(() => {
14138
- void Promise.resolve().then(() => fetchValues(field.name, value)).then(
14237
+ void Promise.resolve().then(() => fetchValues(field.name, value, ac.signal)).then(
14139
14238
  (result) => {
14140
14239
  if (cancelled) return;
14141
14240
  setResponse({ key: requestKey, fetchValues, result });
@@ -14149,6 +14248,7 @@ function FilterValueAutocomplete({ api, field, value, contextKey, className, wid
14149
14248
  return () => {
14150
14249
  cancelled = true;
14151
14250
  clearTimeout(timer);
14251
+ ac.abort();
14152
14252
  };
14153
14253
  }, [open, loaded, fetchValues, requestKey, scopeKey, field.name, value]);
14154
14254
  useEffect21(() => {
@@ -14305,7 +14405,8 @@ function useFieldHasNull(api, fieldName) {
14305
14405
  useEffect22(() => {
14306
14406
  if (!fieldName) return;
14307
14407
  let cancelled = false;
14308
- void api.filterValues(fieldName, "").then((r) => {
14408
+ const ac = new AbortController();
14409
+ void api.filterValues(fieldName, "", ac.signal).then((r) => {
14309
14410
  if (!cancelled && typeof r.hasNull === "boolean") {
14310
14411
  setHasNull(r.hasNull);
14311
14412
  }
@@ -14314,6 +14415,7 @@ function useFieldHasNull(api, fieldName) {
14314
14415
  });
14315
14416
  return () => {
14316
14417
  cancelled = true;
14418
+ ac.abort();
14317
14419
  };
14318
14420
  }, [api.filterValues, fieldName]);
14319
14421
  return hasNull;