@ai-matrx/records-ui 0.110.4 → 0.110.6

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.
@@ -1,4 +1,4 @@
1
- import { Uuid, RecordFilter, RuleExpression, RecordsConfig, RecordPageSort, Table, Field } from '@ai-matrx/records';
1
+ import { Uuid, RecordFilter, RuleExpression, RecordsConfig, StoreFilter, RecordPageSort, Table, Field } from '@ai-matrx/records';
2
2
  import { RecordsClient, RecordsSeed } from '@ai-matrx/records/core';
3
3
  import { TableStyle, GroupAggregateSpec, GroupOrder } from '@ai-matrx/design-system/data-table';
4
4
  import { FieldFormatConfig } from '@ai-matrx/design-system/field-formats';
@@ -324,13 +324,18 @@ declare const TABLE_PAGE_SIZE = 100;
324
324
  declare const EMBEDDED_PAGE_SIZE = 50;
325
325
  interface FirstGridPageArgs {
326
326
  p_table_id: Uuid;
327
- p_filter: Record<string, never>;
327
+ p_filter: StoreFilter | Record<string, never>;
328
328
  p_search: null;
329
329
  p_sort: RecordPageSort[];
330
330
  p_view_id: null;
331
331
  p_limit: number;
332
332
  p_offset: 0;
333
333
  }
334
+ /**
335
+ * THE VIEW'S OWN QUESTION, as the grid asks it (`TablePage` `shownFilter`): its Rule, else its flat filter,
336
+ * through the one question builder. Undefined when it asks none.
337
+ */
338
+ declare function viewFilterOf(shown: Pick<SavedViewSpec, "where" | "filters"> | null, fields: readonly Field[]): StoreFilter | undefined;
334
339
  /** The request for the plain opening, from what the screen itself reads — or null. Pure. */
335
340
  declare function firstGridPageArgs(input: {
336
341
  tableId: Uuid;
@@ -339,6 +344,12 @@ declare function firstGridPageArgs(input: {
339
344
  /** The view as THIS person sees it (the saved view with her look over it), or null for none. */
340
345
  shown: SavedView | null;
341
346
  pageSize?: number;
347
+ /**
348
+ * The host draws the MERGED grid (`RecordsUiProvider grid="merged"`), which keeps a plain grouping as rows
349
+ * under group headers: the page is then read in group order. The classic grid opens the drill for a
350
+ * grouped view, so a grouped view stays unseeded there.
351
+ */
352
+ merged?: boolean;
342
353
  }): FirstGridPageArgs | null;
343
354
  /**
344
355
  * Read what the screen reads (through `client`, e.g. one served from `custom.table_page_bundle`)
@@ -346,6 +357,7 @@ declare function firstGridPageArgs(input: {
346
357
  */
347
358
  declare function firstGridPageRequest(client: Pick<RecordsClient, "tableRead" | "fields" | "views" | "viewLookRead">, tableId: Uuid, options?: {
348
359
  pageSize?: number;
360
+ merged?: boolean;
349
361
  }): Promise<FirstGridPageArgs | null>;
350
362
  /**
351
363
  * A TABLE PAGE'S SEED, ASKED BY THE SERVER AS THE PERSON (lane SSR-ROWS).
@@ -389,6 +401,18 @@ declare function askTablePageSeed(input: {
389
401
  /** The table is drawn EMBEDDED (`TablePage presentation="embedded"`, a Space's inline table): its page size. */
390
402
  embedded?: boolean;
391
403
  pageSize?: number;
404
+ /**
405
+ * The host draws the merged grid. A view grouped by one field (no sub-levels, pivot or drill trail) is then
406
+ * seeded too: the page in group order, the group counts, the summary bar, and the words of the links on the
407
+ * page — each asked with the arguments the grid sends, so the browser's own call finds its answer.
408
+ */
409
+ merged?: boolean;
410
+ /**
411
+ * THE VIEW THE HOST HANDS THE GRID (a Space's own spec: sorts, filters, `where`, grouping), for a table with
412
+ * no saved view behind it. The first page is built from it exactly as the grid reads it; the table's saved
413
+ * views and looks are not asked.
414
+ */
415
+ view?: SavedViewSpec | undefined;
392
416
  }): Promise<RecordsSeed>;
393
417
 
394
- export { EMBEDDED_PAGE_SIZE, type FirstGridPageArgs, TABLE_PAGE_SIZE, type TileSpec, askChartSeed, askEntityBlockSeed, askTablePageSeed, askTileSeed, firstGridPageArgs, firstGridPageRequest, mergeSeeds, serverRowsOf };
418
+ export { EMBEDDED_PAGE_SIZE, type FirstGridPageArgs, TABLE_PAGE_SIZE, type TileSpec, askChartSeed, askEntityBlockSeed, askTablePageSeed, askTileSeed, firstGridPageArgs, firstGridPageRequest, mergeSeeds, serverRowsOf, viewFilterOf };
@@ -1,4 +1,4 @@
1
- import { Uuid, RecordFilter, RuleExpression, RecordsConfig, RecordPageSort, Table, Field } from '@ai-matrx/records';
1
+ import { Uuid, RecordFilter, RuleExpression, RecordsConfig, StoreFilter, RecordPageSort, Table, Field } from '@ai-matrx/records';
2
2
  import { RecordsClient, RecordsSeed } from '@ai-matrx/records/core';
3
3
  import { TableStyle, GroupAggregateSpec, GroupOrder } from '@ai-matrx/design-system/data-table';
4
4
  import { FieldFormatConfig } from '@ai-matrx/design-system/field-formats';
@@ -324,13 +324,18 @@ declare const TABLE_PAGE_SIZE = 100;
324
324
  declare const EMBEDDED_PAGE_SIZE = 50;
325
325
  interface FirstGridPageArgs {
326
326
  p_table_id: Uuid;
327
- p_filter: Record<string, never>;
327
+ p_filter: StoreFilter | Record<string, never>;
328
328
  p_search: null;
329
329
  p_sort: RecordPageSort[];
330
330
  p_view_id: null;
331
331
  p_limit: number;
332
332
  p_offset: 0;
333
333
  }
334
+ /**
335
+ * THE VIEW'S OWN QUESTION, as the grid asks it (`TablePage` `shownFilter`): its Rule, else its flat filter,
336
+ * through the one question builder. Undefined when it asks none.
337
+ */
338
+ declare function viewFilterOf(shown: Pick<SavedViewSpec, "where" | "filters"> | null, fields: readonly Field[]): StoreFilter | undefined;
334
339
  /** The request for the plain opening, from what the screen itself reads — or null. Pure. */
335
340
  declare function firstGridPageArgs(input: {
336
341
  tableId: Uuid;
@@ -339,6 +344,12 @@ declare function firstGridPageArgs(input: {
339
344
  /** The view as THIS person sees it (the saved view with her look over it), or null for none. */
340
345
  shown: SavedView | null;
341
346
  pageSize?: number;
347
+ /**
348
+ * The host draws the MERGED grid (`RecordsUiProvider grid="merged"`), which keeps a plain grouping as rows
349
+ * under group headers: the page is then read in group order. The classic grid opens the drill for a
350
+ * grouped view, so a grouped view stays unseeded there.
351
+ */
352
+ merged?: boolean;
342
353
  }): FirstGridPageArgs | null;
343
354
  /**
344
355
  * Read what the screen reads (through `client`, e.g. one served from `custom.table_page_bundle`)
@@ -346,6 +357,7 @@ declare function firstGridPageArgs(input: {
346
357
  */
347
358
  declare function firstGridPageRequest(client: Pick<RecordsClient, "tableRead" | "fields" | "views" | "viewLookRead">, tableId: Uuid, options?: {
348
359
  pageSize?: number;
360
+ merged?: boolean;
349
361
  }): Promise<FirstGridPageArgs | null>;
350
362
  /**
351
363
  * A TABLE PAGE'S SEED, ASKED BY THE SERVER AS THE PERSON (lane SSR-ROWS).
@@ -389,6 +401,18 @@ declare function askTablePageSeed(input: {
389
401
  /** The table is drawn EMBEDDED (`TablePage presentation="embedded"`, a Space's inline table): its page size. */
390
402
  embedded?: boolean;
391
403
  pageSize?: number;
404
+ /**
405
+ * The host draws the merged grid. A view grouped by one field (no sub-levels, pivot or drill trail) is then
406
+ * seeded too: the page in group order, the group counts, the summary bar, and the words of the links on the
407
+ * page — each asked with the arguments the grid sends, so the browser's own call finds its answer.
408
+ */
409
+ merged?: boolean;
410
+ /**
411
+ * THE VIEW THE HOST HANDS THE GRID (a Space's own spec: sorts, filters, `where`, grouping), for a table with
412
+ * no saved view behind it. The first page is built from it exactly as the grid reads it; the table's saved
413
+ * views and looks are not asked.
414
+ */
415
+ view?: SavedViewSpec | undefined;
392
416
  }): Promise<RecordsSeed>;
393
417
 
394
- export { EMBEDDED_PAGE_SIZE, type FirstGridPageArgs, TABLE_PAGE_SIZE, type TileSpec, askChartSeed, askEntityBlockSeed, askTablePageSeed, askTileSeed, firstGridPageArgs, firstGridPageRequest, mergeSeeds, serverRowsOf };
418
+ export { EMBEDDED_PAGE_SIZE, type FirstGridPageArgs, TABLE_PAGE_SIZE, type TileSpec, askChartSeed, askEntityBlockSeed, askTablePageSeed, askTileSeed, firstGridPageArgs, firstGridPageRequest, mergeSeeds, serverRowsOf, viewFilterOf };
@@ -1,13 +1,22 @@
1
1
  import { recordingDataSource, asRecordsDataSource, createRecordsClient, mapPgError } from '@ai-matrx/records/core';
2
- import { PARITY_FIELD_TYPES, KERNEL_TABLES, mintOpId, fieldKindFor } from '@ai-matrx/records';
3
- import '@ai-matrx/kit/text-case';
4
- import '@ai-matrx/design-system/data-table';
2
+ import { isUuidShape } from '@ai-matrx/kit/uuid';
3
+ import { humanizeIdentifier } from '@ai-matrx/kit/text-case';
4
+ import { PARITY_FIELD_TYPES, KERNEL_TABLES, isRuleFilter, mintOpId, fieldKindFor } from '@ai-matrx/records';
5
+ import { dateFilterBounds, dateFilterDayBounds, dayInstantWindow } from '@ai-matrx/design-system/data-table';
6
+ import 'react';
5
7
  import { parseTableStyle, tableStyleIsEmpty } from '@ai-matrx/design-system/data-table/table-style';
6
8
  import '@ai-matrx/design-system/field-formats';
7
- import { isUuidShape } from '@ai-matrx/kit/uuid';
8
9
  import { defineChannelNamespace } from '@ai-matrx/realtime';
9
10
 
10
11
  // src/firstPage.ts
12
+ function fieldName(field2) {
13
+ const label = (field2.label ?? "").trim();
14
+ if (label !== "" && label === field2.key && /^[a-z0-9]+(?:[_.-][a-z0-9]+)+$/.test(label)) return humanizeIdentifier(label);
15
+ if (label !== "") return label;
16
+ const name = (field2.name ?? "").trim();
17
+ if (name !== "") return name;
18
+ return humanizeIdentifier(field2.key);
19
+ }
11
20
  var kernelId = (name) => KERNEL_TABLES.find((t) => t.name === name)?.id ?? "";
12
21
  var KERNEL_FILE_TABLE = kernelId("File");
13
22
  var KERNEL_PERSON_TABLE = kernelId("Person");
@@ -15,8 +24,8 @@ Object.fromEntries(
15
24
  PARITY_FIELD_TYPES.map((p) => [p.parity_type, p.made_of])
16
25
  );
17
26
  var KERNEL = { file: KERNEL_FILE_TABLE, person: KERNEL_PERSON_TABLE };
18
- function editorKindFor(field) {
19
- return fieldKindFor(field, KERNEL);
27
+ function editorKindFor(field2) {
28
+ return fieldKindFor(field2, KERNEL);
20
29
  }
21
30
 
22
31
  // src/gridLook.ts
@@ -43,25 +52,212 @@ function defaultSortOf(table, fields) {
43
52
  if (String(entry["direction"] ?? "").toLowerCase() === "desc") direction = "desc";
44
53
  }
45
54
  if (!name) return null;
46
- const field = fields.find((f) => f.key === name) ?? fields.find((f) => f.id === name);
47
- return field ? { id: field.key, direction } : null;
55
+ const field2 = fields.find((f) => f.key === name) ?? fields.find((f) => f.id === name);
56
+ return field2 ? { id: field2.key, direction } : null;
57
+ }
58
+ var MEASURE_LABEL = {
59
+ count: "Count",
60
+ sum: "Sum",
61
+ avg: "Average",
62
+ min: "Smallest",
63
+ max: "Largest",
64
+ median: "Median",
65
+ filled: "Filled",
66
+ empty: "Empty",
67
+ unique: "Unique",
68
+ percent_empty: "Percent empty",
69
+ percent_filled: "Percent filled",
70
+ range: "Range"
71
+ };
72
+
73
+ // src/gridPage.ts
74
+ function filterRefusal(f) {
75
+ if (!computedOnRead(f)) return null;
76
+ return `${fieldName(f)} is worked out each time it is read, so the store keeps no value to filter the whole table by. Filter by a column it is worked out from.`;
77
+ }
78
+ function computedOnRead(f) {
79
+ const kind = String(editorKindFor(f));
80
+ return (f.type === "formula" || kind === "formula" || kind === "lookup" || kind === "rollup") && String(f.compute_on ?? "read") === "read";
48
81
  }
49
- function holdsNumbers(field) {
50
- const kind = String(editorKindFor(field));
82
+ function holdsNumbers(field2) {
83
+ const kind = String(editorKindFor(field2));
51
84
  if (kind === "number" || kind === "currency" || kind === "percent") return true;
52
- return String((field.config ?? {})["system"] ?? "") === "autonumber";
85
+ return String((field2.config ?? {})["system"] ?? "") === "autonumber";
53
86
  }
54
- function sortAsFor(field) {
55
- const kind = String(editorKindFor(field));
56
- if (holdsNumbers(field)) return "number";
87
+ function sortAsFor(field2) {
88
+ const kind = String(editorKindFor(field2));
89
+ if (holdsNumbers(field2)) return "number";
57
90
  if (kind === "datetime") return "date";
58
91
  return "text";
59
92
  }
60
93
  function pageSortFor(fields, sort) {
61
94
  if (!sort) return [];
62
- const field = fields.find((f) => f.key === sort.id);
63
- if (!field) return [];
64
- return [{ field: field.key, direction: sort.direction === "desc" ? "desc" : "asc", as: sortAsFor(field) }];
95
+ const field2 = fields.find((f) => f.key === sort.id);
96
+ if (!field2) return [];
97
+ return [{ field: field2.key, direction: sort.direction === "desc" ? "desc" : "asc", as: sortAsFor(field2) }];
98
+ }
99
+ function containsPattern(text) {
100
+ return `(?i)${text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`;
101
+ }
102
+ var BLANK_VALUE = "__empty__";
103
+ var field = (f) => ({ field: f.id });
104
+ var constant = (value) => ({ const: value });
105
+ var withinWindow = (f, window) => ({ op: "within", args: [field(f), constant(window)] });
106
+ var op = (name, ...args) => ({ op: name, args });
107
+ function clauseFor(f, value, now = /* @__PURE__ */ new Date()) {
108
+ const name = fieldName(f);
109
+ const refused = filterRefusal(f);
110
+ if (refused) return refused;
111
+ const not = (rule) => op("not", rule);
112
+ switch (value.kind) {
113
+ case "text": {
114
+ const mode = value.mode ?? "contains";
115
+ if (mode === "empty" || mode === "null") return value.negated ? { rule: op("present", field(f)) } : { rule: not(op("present", field(f))) };
116
+ if (mode === "not_empty" || mode === "not_null") return value.negated ? { rule: not(op("present", field(f))) } : { rule: op("present", field(f)) };
117
+ const text = value.value.trim();
118
+ if (text === "") return null;
119
+ const kind = String(editorKindFor(f));
120
+ if (kind === "number" || kind === "currency" || kind === "percent" || kind === "checkbox") {
121
+ return `${name} holds numbers, so it is filtered by a range, not by words.`;
122
+ }
123
+ const matches = op("matches", field(f), constant(containsPattern(text)));
124
+ return { rule: value.negated ? not(matches) : matches };
125
+ }
126
+ case "select": {
127
+ const ticked = value.values ?? (value.value === "" ? [] : [value.value]);
128
+ const blank = ticked.includes(BLANK_VALUE);
129
+ const words = ticked.filter((w) => w !== BLANK_VALUE);
130
+ if (!blank && words.length === 0) return value.negated ? null : { rule: constant(false) };
131
+ const present = op("present", field(f));
132
+ const kind = String(editorKindFor(f));
133
+ if (kind === "datetime" || kind === "date") {
134
+ if (words.length === 0) return { rule: value.negated ? present : not(present) };
135
+ const holdsDays = String((f.config ?? {})["kind"] ?? "") === "date";
136
+ if (words.length === 1 && !blank && !value.negated) return { flat: { [f.key]: dayWindow(words[0], holdsDays) } };
137
+ const leaves = words.map((day) => withinWindow(f, dayWindow(day, holdsDays)));
138
+ const anyDay = leaves.length === 1 ? leaves[0] : op("or", ...leaves);
139
+ if (!value.negated) return { rule: blank ? op("or", not(present), anyDay) : anyDay };
140
+ return { rule: blank ? op("and", present, not(anyDay)) : not(anyDay) };
141
+ }
142
+ const asStored = (w) => holdsNumbers(f) || kind === "rating" ? Number.isFinite(Number(w)) ? Number(w) : w : kind === "checkbox" && (w === "true" || w === "false") ? w === "true" : w;
143
+ const stored = words.map(asStored);
144
+ const anyWord = stored.length === 0 ? null : stored.length === 1 ? op("eq", field(f), constant(stored[0])) : op("or", ...stored.map((w) => op("eq", field(f), constant(w))));
145
+ if (!value.negated) {
146
+ if (!blank && stored.length === 1) return { flat: { [f.key]: stored[0] } };
147
+ if (!blank) return { rule: anyWord };
148
+ return { rule: anyWord ? op("or", not(present), anyWord) : not(present) };
149
+ }
150
+ if (blank) return { rule: anyWord ? op("and", present, not(anyWord)) : present };
151
+ return { rule: op("or", not(present), not(anyWord)) };
152
+ }
153
+ case "boolean":
154
+ return value.negated ? { rule: op("ne", field(f), constant(value.value)) } : { flat: { [f.key]: value.value } };
155
+ case "number": {
156
+ const turned = (rule) => ({ rule: value.negated ? not(rule) : rule });
157
+ if (value.op === "empty") return turned(not(op("present", field(f))));
158
+ if (value.op === "not_empty") return turned(op("present", field(f)));
159
+ if (value.op === "eq" || value.op === "ne") {
160
+ if (typeof value.min !== "number") return null;
161
+ return turned(op(value.op, field(f), constant(value.min)));
162
+ }
163
+ if (value.op === "lt") return typeof value.max === "number" ? turned(op("lt", field(f), constant(value.max))) : null;
164
+ if (value.op === "gt") return typeof value.min === "number" ? turned(op("gt", field(f), constant(value.min))) : null;
165
+ const parts = [];
166
+ if (typeof value.min === "number") parts.push(op("gte", field(f), constant(value.min)));
167
+ if (typeof value.max === "number") parts.push(op("lte", field(f), constant(value.max)));
168
+ if (parts.length === 0) return null;
169
+ const range = parts.length === 1 ? parts[0] : op("and", ...parts);
170
+ return { rule: value.negated ? not(range) : range };
171
+ }
172
+ case "date": {
173
+ const zone2 = void 0;
174
+ const moments = dateFilterBounds(value.preset ? { ...value, anchor: now.toISOString() } : value, now, zone2);
175
+ const holdsDays = String((f.config ?? {})["kind"] ?? "") === "date";
176
+ const bounds = holdsDays ? dateFilterDayBounds(moments, zone2) : moments;
177
+ if (bounds.since === void 0 && bounds.until === void 0) return null;
178
+ return {
179
+ flat: {
180
+ [f.key]: {
181
+ ...bounds.since === void 0 ? {} : { from: bounds.since },
182
+ ...bounds.until === void 0 ? {} : { to: bounds.until }
183
+ }
184
+ }
185
+ };
186
+ }
187
+ default:
188
+ return `${name} cannot be filtered that way by the store.`;
189
+ }
190
+ }
191
+ function dayWindow(day, holdsDays) {
192
+ if (!holdsDays) return dayInstantWindow(day, void 0);
193
+ const [y, m, d] = day.split("-").map(Number);
194
+ const ymd = (t) => `${t.getUTCFullYear()}-${String(t.getUTCMonth() + 1).padStart(2, "0")}-${String(t.getUTCDate()).padStart(2, "0")}`;
195
+ return { from: ymd(new Date(Date.UTC(y, (m ?? 1) - 1, d ?? 1))), to: ymd(new Date(Date.UTC(y, (m ?? 1) - 1, (d ?? 1) + 1))) };
196
+ }
197
+ function storeQuestion(fields, base, columnFilters, now = /* @__PURE__ */ new Date()) {
198
+ const refused = [];
199
+ const flat = {};
200
+ const rules = [];
201
+ if (base && isRuleFilter(base)) rules.push(base);
202
+ else if (base) Object.assign(flat, base);
203
+ for (const [key, value] of Object.entries(columnFilters)) {
204
+ if (!value) continue;
205
+ const f = fields.find((candidate) => candidate.key === key);
206
+ if (!f) continue;
207
+ const clause = clauseFor(f, value, now);
208
+ if (clause === null) continue;
209
+ if (typeof clause === "string") {
210
+ refused.push(clause);
211
+ continue;
212
+ }
213
+ if ("flat" in clause) Object.assign(flat, clause.flat);
214
+ else rules.push(clause.rule);
215
+ }
216
+ const flatKeys = Object.keys(flat);
217
+ if (rules.length === 0) return { filter: flatKeys.length > 0 ? flat : void 0, refused };
218
+ for (const key of flatKeys) {
219
+ const f = fields.find((candidate) => candidate.key === key);
220
+ const value = flat[key];
221
+ if (!f) continue;
222
+ if (value !== null && typeof value === "object") {
223
+ rules.push(withinWindow(f, value));
224
+ continue;
225
+ }
226
+ rules.push(value === null ? op("not", op("present", field(f))) : op("eq", field(f), constant(value)));
227
+ }
228
+ return { filter: rules.length === 1 ? rules[0] : op("and", ...rules), refused };
229
+ }
230
+
231
+ // src/rowVersion.ts
232
+ function ownVersion(row) {
233
+ const v = row.version;
234
+ return typeof v === "number" ? v : null;
235
+ }
236
+
237
+ // src/groupReads.ts
238
+ function isPlainGrouping(grouping) {
239
+ return Boolean(grouping) && !(grouping.then && grouping.then.length > 0) && !grouping.across && !(grouping.trail && grouping.trail.length > 0);
240
+ }
241
+ function groupedPageSort(fields, groupKey, sort) {
242
+ if (!groupKey) return sort;
243
+ const field2 = fields.find((f) => f.key === groupKey);
244
+ if (!field2) return sort;
245
+ return [{ field: groupKey, direction: "asc", as: sortAsFor(field2) }, ...sort.filter((s) => s.field !== groupKey)];
246
+ }
247
+ function chosenEntries(chosen) {
248
+ return JSON.parse(JSON.stringify(Object.entries(chosen).sort())).filter(([, op2]) => op2 in MEASURE_LABEL);
249
+ }
250
+ function groupFactMeasures(chosen) {
251
+ return [{ op: "count" }, ...chosenEntries(chosen).filter(([, op2]) => op2 !== "count").map(([key, op2]) => ({ op: op2, key }))];
252
+ }
253
+ function summaryBarMeasures(chosen) {
254
+ return chosenEntries(chosen).map(([key, op2]) => op2 === "count" ? { op: "count" } : { op: op2, key });
255
+ }
256
+ function choiceFieldKeys(fields) {
257
+ return fields.filter((f) => {
258
+ const kind = String(editorKindFor(f));
259
+ return kind === "select" || kind === "multi_select";
260
+ }).map((f) => f.key);
65
261
  }
66
262
  var MIN_ROW_HEIGHT = 24;
67
263
  var MAX_ROW_HEIGHT = 96;
@@ -161,8 +357,8 @@ function parseGridPresentation(raw) {
161
357
  if (footer === "sticky" || footer === "inline") out.footer = footer;
162
358
  if (isRecord(value["summaries"])) {
163
359
  const summaries = {};
164
- for (const [key, op] of Object.entries(value["summaries"])) {
165
- if (typeof op === "string" && op.trim() !== "") summaries[key] = op;
360
+ for (const [key, op2] of Object.entries(value["summaries"])) {
361
+ if (typeof op2 === "string" && op2.trim() !== "") summaries[key] = op2;
166
362
  }
167
363
  if (Object.keys(summaries).length > 0) out.summaries = summaries;
168
364
  }
@@ -170,9 +366,9 @@ function parseGridPresentation(raw) {
170
366
  }
171
367
  function parseGrouping(raw) {
172
368
  if (!isRecord(raw)) return void 0;
173
- const field = raw["field"];
174
- if (typeof field !== "string" || field.trim() === "") return void 0;
175
- const out = { field };
369
+ const field2 = raw["field"];
370
+ if (typeof field2 !== "string" || field2.trim() === "") return void 0;
371
+ const out = { field: field2 };
176
372
  if (isRecord(raw["aggregates"])) {
177
373
  const aggregates = {};
178
374
  for (const [key, spec] of Object.entries(raw["aggregates"])) {
@@ -952,39 +1148,68 @@ async function askTileSeed(input) {
952
1148
  // src/firstPage.ts
953
1149
  var TABLE_PAGE_SIZE = 100;
954
1150
  var EMBEDDED_PAGE_SIZE = 50;
1151
+ function viewFilterOf(shown, fields) {
1152
+ if (!shown) return void 0;
1153
+ const base = shown.where ?? (shown.filters && Object.keys(shown.filters).length > 0 ? shown.filters : void 0);
1154
+ return storeQuestion(fields, base, {}).filter;
1155
+ }
955
1156
  function firstGridPageArgs(input) {
956
1157
  const { shown, fields } = input;
957
1158
  if (!shown) return null;
958
1159
  if ((shown.layout ?? "grid") !== "grid") return null;
959
1160
  if (shown.ruleId) return null;
960
1161
  if (shown.order === "manual") return null;
961
- if (shown.filters && Object.keys(shown.filters).length > 0) return null;
962
- if (shown.presentation?.grouping) return null;
1162
+ const grouping = shown.presentation?.grouping;
1163
+ if (grouping && !(input.merged && isPlainGrouping(grouping))) return null;
963
1164
  const first = (shown.sorts ?? [])[0];
964
1165
  const viewSort = first && fields.some((f) => f.key === first.field) ? { id: first.field, direction: first.direction } : null;
965
1166
  const opening = viewSort ?? defaultSortOf(input.table, fields);
966
1167
  return {
967
1168
  p_table_id: input.tableId,
968
- p_filter: {},
1169
+ p_filter: viewFilterOf(shown, fields) ?? {},
969
1170
  p_search: null,
970
- p_sort: pageSortFor(fields, opening),
1171
+ p_sort: groupedPageSort(fields, grouping?.field ?? null, pageSortFor(fields, opening)),
971
1172
  p_view_id: null,
972
1173
  p_limit: input.pageSize ?? TABLE_PAGE_SIZE,
973
1174
  p_offset: 0
974
1175
  };
975
1176
  }
976
1177
  async function firstGridPageRequest(client, tableId, options = {}) {
1178
+ return (await firstGridOpening(client, tableId, options))?.args ?? null;
1179
+ }
1180
+ function defaultSpec(tableId) {
1181
+ return { name: "All", subject: tableId, layout: "grid", isDefault: true };
1182
+ }
1183
+ function shownFromSpec(spec, tableId) {
1184
+ return { ...spec, subject: spec.subject ?? tableId, id: "", movedFrom: null };
1185
+ }
1186
+ async function firstGridOpening(client, tableId, options) {
1187
+ const explicit = options.view;
977
1188
  const [table, fields, views, looks] = await Promise.all([
978
1189
  client.tableRead({ table_id: tableId }),
979
1190
  client.fields({ table_id: tableId }),
980
- client.views({ table_id: tableId }),
981
- readLooks(client, tableId)
1191
+ explicit ? Promise.resolve(null) : client.views({ table_id: tableId }),
1192
+ explicit ? Promise.resolve(null) : readLooks(client, tableId)
982
1193
  ]);
983
- if (!table.ok || !fields.ok || !views.ok || !looks.ok) return null;
984
- const mine = views.data.map(viewFromRow).filter((v) => v.subject === tableId);
985
- const view = defaultViewOf(mine) ?? mine[0] ?? null;
986
- const shown = view ? applyLook(view, looks.data[view.id]) : null;
987
- return firstGridPageArgs({ tableId, table: table.data, fields: fields.data, shown, ...options.pageSize ? { pageSize: options.pageSize } : {} });
1194
+ if (!table.ok || !fields.ok) return null;
1195
+ let shown;
1196
+ if (explicit) {
1197
+ shown = shownFromSpec(explicit, tableId);
1198
+ } else {
1199
+ if (!views?.ok || !looks?.ok) return null;
1200
+ const mine = views.data.map(viewFromRow).filter((v) => v.subject === tableId);
1201
+ const view = defaultViewOf(mine) ?? mine[0] ?? null;
1202
+ shown = view ? applyLook(view, looks.data[view.id]) : shownFromSpec(defaultSpec(tableId), tableId);
1203
+ }
1204
+ const args = firstGridPageArgs({
1205
+ tableId,
1206
+ table: table.data,
1207
+ fields: fields.data,
1208
+ shown,
1209
+ ...options.pageSize ? { pageSize: options.pageSize } : {},
1210
+ ...options.merged ? { merged: true } : {}
1211
+ });
1212
+ return args ? { args, shown, fields: fields.data } : null;
988
1213
  }
989
1214
  function serverRowsOf(seed) {
990
1215
  const found = seed?.answers.find((a) => a.door === "server_rows");
@@ -1006,10 +1231,11 @@ async function askTablePageSeed(input) {
1006
1231
  ).catch(() => null) : null;
1007
1232
  try {
1008
1233
  const pageSize = input.pageSize ?? (input.embedded ? EMBEDDED_PAGE_SIZE : TABLE_PAGE_SIZE);
1009
- const args = await firstGridPageRequest(client, input.tableId, { pageSize });
1234
+ const opening = await firstGridOpening(client, input.tableId, { pageSize, ...input.merged ? { merged: true } : {}, ...input.view ? { view: input.view } : {} });
1010
1235
  const wantRows = typeof input.rows === "function" ? input.rows(recording.seed()) : input.rows !== false;
1011
- if (args && wantRows) {
1012
- await client.listPage({
1236
+ if (opening && wantRows) {
1237
+ const { args, shown, fields } = opening;
1238
+ const page = await client.listPage({
1013
1239
  table_id: args.p_table_id,
1014
1240
  filter: args.p_filter,
1015
1241
  search: args.p_search,
@@ -1018,6 +1244,34 @@ async function askTablePageSeed(input) {
1018
1244
  limit: args.p_limit,
1019
1245
  offset: args.p_offset
1020
1246
  });
1247
+ if (page.ok) {
1248
+ const chosen = shown?.presentation?.summaries ?? {};
1249
+ const groupKey = shown?.presentation?.grouping?.field ?? null;
1250
+ const reads = [];
1251
+ const filter = args.p_filter && Object.keys(args.p_filter).length > 0 ? args.p_filter : void 0;
1252
+ if (groupKey) {
1253
+ reads.push(client.recordAggregate({ table_id: input.tableId, groupBy: [groupKey], measures: groupFactMeasures(chosen), limit: 1e3, ...filter ? { filter } : {} }));
1254
+ }
1255
+ const bar = summaryBarMeasures(chosen);
1256
+ if (bar.length > 0) reads.push(client.recordAggregate({ table_id: input.tableId, measures: bar, ...filter ? { filter } : {} }));
1257
+ for (const field2 of fields) {
1258
+ if (field2.type !== "list" && field2.type !== "relation") continue;
1259
+ const ids = /* @__PURE__ */ new Set();
1260
+ for (const row of page.data.rows) {
1261
+ const value = row.document?.[field2.key];
1262
+ for (const id of Array.isArray(value) ? value.map(String) : typeof value === "string" ? [value] : []) if (isUuidShape(id)) ids.add(id);
1263
+ }
1264
+ if (ids.size > 0) reads.push(client.relationWordsMany({ field_id: field2.id, record_ids: [...ids] }));
1265
+ }
1266
+ if (input.merged) {
1267
+ for (const key of choiceFieldKeys(fields)) {
1268
+ reads.push(client.recordAggregate({ table_id: input.tableId, groupBy: [key], measures: [{ op: "count" }], limit: 500, ...filter ? { filter } : {} }));
1269
+ }
1270
+ }
1271
+ const unversioned = page.data.rows.filter((row) => ownVersion(row) === null).map((row) => row.id);
1272
+ if (unversioned.length > 0) reads.push(client.recordHeaders({ ids: unversioned }));
1273
+ await Promise.all(reads.map((read) => read.catch(() => null)));
1274
+ }
1021
1275
  }
1022
1276
  } catch {
1023
1277
  }
@@ -1025,6 +1279,6 @@ async function askTablePageSeed(input) {
1025
1279
  return recording.seed();
1026
1280
  }
1027
1281
 
1028
- export { EMBEDDED_PAGE_SIZE, TABLE_PAGE_SIZE, askChartSeed, askEntityBlockSeed, askTablePageSeed, askTileSeed, firstGridPageArgs, firstGridPageRequest, mergeSeeds, serverRowsOf };
1282
+ export { EMBEDDED_PAGE_SIZE, TABLE_PAGE_SIZE, askChartSeed, askEntityBlockSeed, askTablePageSeed, askTileSeed, firstGridPageArgs, firstGridPageRequest, mergeSeeds, serverRowsOf, viewFilterOf };
1029
1283
  //# sourceMappingURL=first-page.js.map
1030
1284
  //# sourceMappingURL=first-page.js.map