gbs-add-block 2.0.4 → 2.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/.gbs/skills/gbs-components/SKILL.md +190 -134
- package/.gbs/skills/gbs-components/references/install.md +25 -3
- package/.gbs/skills/gbs-components/references/styling.md +246 -201
- package/CHANGELOG.md +74 -0
- package/README.md +157 -11
- package/index.cjs +212 -3
- package/package.json +41 -10
- package/schema/passport-v1.schema.json +204 -0
- package/source/beta-components/accordion/passport.json +259 -0
- package/source/beta-components/accordion/styles.css +207 -208
- package/source/beta-components/alert/passport.json +250 -0
- package/source/beta-components/alert/styles.css +154 -155
- package/source/beta-components/avatar/passport.json +294 -0
- package/source/beta-components/avatar/styles.css +201 -203
- package/source/beta-components/badge/passport.json +332 -0
- package/source/beta-components/badge/styles.css +203 -204
- package/source/beta-components/breadcrumb/passport.json +243 -0
- package/source/beta-components/breadcrumb/styles.css +138 -139
- package/source/beta-components/button/passport.json +402 -0
- package/source/beta-components/button/passport.manual.json +31 -0
- package/source/beta-components/button/styles.css +232 -233
- package/source/beta-components/card/passport.json +337 -0
- package/source/beta-components/card/styles.css +230 -231
- package/source/beta-components/checkbox/passport.json +456 -0
- package/source/beta-components/checkbox/styles.css +211 -212
- package/source/beta-components/combobox/passport.json +456 -0
- package/source/beta-components/combobox/styles.css +419 -417
- package/source/beta-components/data-grid/agent/coerce.ts +368 -0
- package/source/beta-components/data-grid/agent/contract.ts +410 -0
- package/source/beta-components/data-grid/agent/dataset.ts +92 -0
- package/source/beta-components/data-grid/agent/engine.ts +470 -0
- package/source/beta-components/data-grid/agent/executors.ts +155 -0
- package/source/beta-components/data-grid/agent/index.ts +79 -0
- package/source/beta-components/data-grid/agent/intent.ts +324 -0
- package/source/beta-components/data-grid/agent/operations.ts +335 -0
- package/source/beta-components/data-grid/agent/validate.ts +630 -0
- package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
- package/source/beta-components/data-grid/index.ts +14 -7
- package/source/beta-components/data-grid/passport.json +1051 -0
- package/source/beta-components/data-grid/passport.manual.json +255 -0
- package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
- package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
- package/source/beta-components/data-grid/styles.css +874 -717
- package/source/beta-components/date-picker/passport.json +407 -0
- package/source/beta-components/date-picker/styles.css +445 -446
- package/source/beta-components/dialog/passport.json +344 -0
- package/source/beta-components/dialog/styles.css +280 -278
- package/source/beta-components/file-uploader/passport.json +518 -0
- package/source/beta-components/file-uploader/styles.css +394 -395
- package/source/beta-components/input/passport.json +536 -0
- package/source/beta-components/input/styles.css +295 -296
- package/source/beta-components/menu/passport.json +322 -0
- package/source/beta-components/menu/styles.css +224 -222
- package/source/beta-components/modal/passport.json +289 -0
- package/source/beta-components/modal/styles.css +241 -239
- package/source/beta-components/number-input/passport.json +541 -0
- package/source/beta-components/number-input/styles.css +230 -231
- package/source/beta-components/popover/passport.json +238 -0
- package/source/beta-components/popover/styles.css +148 -146
- package/source/beta-components/progress/passport.json +270 -0
- package/source/beta-components/progress/styles.css +200 -202
- package/source/beta-components/radio-group/passport.json +477 -0
- package/source/beta-components/radio-group/styles.css +241 -242
- package/source/beta-components/shared/core/agent/adapter.ts +65 -0
- package/source/beta-components/shared/core/agent/history.ts +120 -0
- package/source/beta-components/shared/core/agent/index.ts +46 -0
- package/source/beta-components/shared/core/agent/numbers.ts +217 -0
- package/source/beta-components/shared/core/agent/schema.ts +180 -0
- package/source/beta-components/shared/core/agent/types.ts +169 -0
- package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
- package/source/beta-components/shared/index.ts +9 -0
- package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
- package/source/beta-components/shared/react/useAskAgent.ts +217 -0
- package/source/beta-components/shared/styles.css +79 -0
- package/source/beta-components/shared/version.json +4 -4
- package/source/beta-components/shared/version.ts +6 -6
- package/source/beta-components/skeleton/passport.json +251 -0
- package/source/beta-components/skeleton/styles.css +185 -186
- package/source/beta-components/spinner/passport.json +245 -0
- package/source/beta-components/spinner/styles.css +145 -146
- package/source/beta-components/switch/passport.json +421 -0
- package/source/beta-components/switch/styles.css +194 -196
- package/source/beta-components/tabs/passport.json +315 -0
- package/source/beta-components/tabs/styles.css +234 -235
- package/source/beta-components/textarea/passport.json +382 -0
- package/source/beta-components/textarea/styles.css +158 -159
- package/source/beta-components/toaster/passport.json +221 -0
- package/source/beta-components/toaster/styles.css +282 -283
- package/source/beta-components/tooltip/passport.json +170 -0
- package/source/beta-components/tooltip/styles.css +71 -72
- package/tools/env.cjs +61 -0
- package/tools/passport/cli.cjs +79 -0
- package/tools/passport/extract.cjs +493 -0
- package/tools/passport/index.cjs +185 -0
- package/tools/passport/merge.cjs +131 -0
- package/tools/passport/policy.cjs +65 -0
- package/tools/passport/validate.cjs +277 -0
- package/tools/ts-require.cjs +79 -0
- package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
- package/source/beta-components/alert/__tests__/core.test.ts +0 -17
- package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
- package/source/beta-components/badge/__tests__/core.test.ts +0 -46
- package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
- package/source/beta-components/button/__tests__/core.test.ts +0 -31
- package/source/beta-components/card/__tests__/core.test.ts +0 -57
- package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
- package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
- package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
- package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
- package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
- package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
- package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
- package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
- package/source/beta-components/input/__tests__/core.test.ts +0 -75
- package/source/beta-components/menu/__tests__/core.test.ts +0 -120
- package/source/beta-components/modal/__tests__/core.test.ts +0 -55
- package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
- package/source/beta-components/progress/__tests__/core.test.ts +0 -56
- package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
- package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
- package/source/beta-components/shared/__tests__/core.test.ts +0 -55
- package/source/beta-components/shared/__tests__/position.test.ts +0 -143
- package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
- package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
- package/source/beta-components/switch/__tests__/core.test.ts +0 -64
- package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
- package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
- package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
- package/source/beta-components/tooltip/__tests__/core.test.ts +0 -42
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The runtime contract: what *this* grid can do, right now.
|
|
3
|
+
*
|
|
4
|
+
* The passport says "DataGrid can filter, sort, search, select and export".
|
|
5
|
+
* That is true of every DataGrid ever rendered and so tells a caller nothing
|
|
6
|
+
* about the one in front of it. The runtime contract is the other half:
|
|
7
|
+
*
|
|
8
|
+
* columns revenue is a number, filterable, measured in INR
|
|
9
|
+
* capabilities this instance has selection off and pinning on
|
|
10
|
+
* state two filters and a descending sort are active
|
|
11
|
+
* stats 1,842 of 9,310 rows match
|
|
12
|
+
*
|
|
13
|
+
* Three rules hold throughout:
|
|
14
|
+
*
|
|
15
|
+
* - No rows. Counts and bounded summaries only. A contract is the thing
|
|
16
|
+
* most likely to be handed to a model, and rows are the thing most likely
|
|
17
|
+
* to be regretted.
|
|
18
|
+
* - Nothing invented. Every capability traces to a grid option or a
|
|
19
|
+
* resolved column flag. There is no `grouping`, because there is no
|
|
20
|
+
* grouping.
|
|
21
|
+
* - Effective, not nominal. Policy is applied here, so a column the host
|
|
22
|
+
* forbade reports `filterable: false` and carries no summary.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { getFilterOperators } from "../core/filtering";
|
|
26
|
+
import type {
|
|
27
|
+
ColumnOption,
|
|
28
|
+
ColumnType,
|
|
29
|
+
Density,
|
|
30
|
+
FilterOperator,
|
|
31
|
+
FilterValue,
|
|
32
|
+
GridOptions,
|
|
33
|
+
ResolvedColumn,
|
|
34
|
+
} from "../core/types";
|
|
35
|
+
import { toDateInputValue, toTime } from "../core/values";
|
|
36
|
+
import type { GridDataset } from "./dataset";
|
|
37
|
+
import { GRID_OPERATIONS, type GridOperationName } from "./operations";
|
|
38
|
+
|
|
39
|
+
export const GRID_CONTRACT_VERSION = "1.0.0";
|
|
40
|
+
|
|
41
|
+
// ------------------------------------------------------------------ authored
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What a machine cannot read off the column definition. Supplied by the host;
|
|
45
|
+
* nothing here is guessed, and an absent entry simply means less context.
|
|
46
|
+
*/
|
|
47
|
+
export interface GridColumnSemantics {
|
|
48
|
+
/** What the column means, in words, for a model's prompt. */
|
|
49
|
+
description?: string;
|
|
50
|
+
/** Display unit. An ISO 4217 code such as `INR` also drives money formatting. */
|
|
51
|
+
unit?: string;
|
|
52
|
+
/** How a percent column stores its numbers: 20 (`whole`) or 0.2 (`fraction`). */
|
|
53
|
+
percentBasis?: "whole" | "fraction";
|
|
54
|
+
/** Which end is the good end. Lets "worst performing" resolve to a direction. */
|
|
55
|
+
higherIsBetter?: boolean;
|
|
56
|
+
/** Other words a person might use for this column. */
|
|
57
|
+
synonyms?: readonly string[];
|
|
58
|
+
/** Personal data. Denied by default — see `GridAgentPolicy.denyPii`. */
|
|
59
|
+
pii?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface GridAgentPolicy {
|
|
63
|
+
/** Columns no operation may touch. */
|
|
64
|
+
deny?: readonly string[];
|
|
65
|
+
/** Columns that may not be filtered or searched on. */
|
|
66
|
+
denyFilter?: readonly string[];
|
|
67
|
+
/** Treat columns marked `pii` as denied. Default true. */
|
|
68
|
+
denyPii?: boolean;
|
|
69
|
+
/** Operations withheld from this instance. */
|
|
70
|
+
denyOperations?: readonly GridOperationName[];
|
|
71
|
+
/** Refuse an export of more rows than this. */
|
|
72
|
+
maxExportRows?: number;
|
|
73
|
+
/** Ask before exporting more rows than this. Default 5000. */
|
|
74
|
+
confirmExportRows?: number;
|
|
75
|
+
allowExportFormats?: readonly ("csv" | "excel" | "pdf")[];
|
|
76
|
+
/** Rows one `selectRows` may name. Default 1000. */
|
|
77
|
+
maxSelectRows?: number;
|
|
78
|
+
/** Warn when a filter would leave nothing. Default true. */
|
|
79
|
+
warnOnEmptyResult?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ------------------------------------------------------------------ contract
|
|
83
|
+
|
|
84
|
+
export type GridColumnStats =
|
|
85
|
+
| { kind: "number"; min: number; max: number; nulls: number }
|
|
86
|
+
| { kind: "date"; min: string; max: string; nulls: number }
|
|
87
|
+
| { kind: "boolean"; true: number; false: number; nulls: number }
|
|
88
|
+
| { kind: "string"; distinct: number; nulls: number; values?: string[] };
|
|
89
|
+
|
|
90
|
+
export interface GridContractColumn {
|
|
91
|
+
id: string;
|
|
92
|
+
label: string;
|
|
93
|
+
type: ColumnType;
|
|
94
|
+
visible: boolean;
|
|
95
|
+
pinned: "left" | "right" | null;
|
|
96
|
+
sortable: boolean;
|
|
97
|
+
filterable: boolean;
|
|
98
|
+
searchable: boolean;
|
|
99
|
+
hideable: boolean;
|
|
100
|
+
pinnable: boolean;
|
|
101
|
+
reorderable: boolean;
|
|
102
|
+
resizable: boolean;
|
|
103
|
+
exportable: boolean;
|
|
104
|
+
editable: boolean;
|
|
105
|
+
/** Operators legal for this column's type. Empty when it cannot be filtered. */
|
|
106
|
+
operators: FilterOperator[];
|
|
107
|
+
/** Fixed choices from the column definition. */
|
|
108
|
+
options?: ColumnOption[];
|
|
109
|
+
unit?: string;
|
|
110
|
+
percentBasis?: "whole" | "fraction";
|
|
111
|
+
higherIsBetter?: boolean;
|
|
112
|
+
synonyms?: string[];
|
|
113
|
+
description?: string;
|
|
114
|
+
/** Present when policy, not the grid, is what closed this column down. */
|
|
115
|
+
restricted?: "policy";
|
|
116
|
+
stats?: GridColumnStats;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface GridCapabilities {
|
|
120
|
+
mode: "client" | "server";
|
|
121
|
+
sorting: boolean;
|
|
122
|
+
multiSort: boolean;
|
|
123
|
+
filtering: boolean;
|
|
124
|
+
globalSearch: boolean;
|
|
125
|
+
pagination: boolean;
|
|
126
|
+
selection: boolean;
|
|
127
|
+
selectionMode: "single" | "multiple";
|
|
128
|
+
columnResizing: boolean;
|
|
129
|
+
columnReordering: boolean;
|
|
130
|
+
columnPinning: boolean;
|
|
131
|
+
columnHiding: boolean;
|
|
132
|
+
/** Cells can be edited in place. No operation exposes this; it is context. */
|
|
133
|
+
editing: boolean;
|
|
134
|
+
exportFormats: ("csv" | "excel" | "pdf")[];
|
|
135
|
+
print: boolean;
|
|
136
|
+
clipboard: boolean;
|
|
137
|
+
undo: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface GridContractState {
|
|
141
|
+
sorting: { column: string; direction: "asc" | "desc" }[];
|
|
142
|
+
filters: { column: string; operator: FilterOperator; value?: FilterValue; value2?: FilterValue }[];
|
|
143
|
+
search: string;
|
|
144
|
+
page: { index: number; size: number; count: number };
|
|
145
|
+
selection: { count: number; all: boolean };
|
|
146
|
+
density: Density;
|
|
147
|
+
hiddenColumns: string[];
|
|
148
|
+
pinnedColumns: { left: string[]; right: string[] };
|
|
149
|
+
canUndo: boolean;
|
|
150
|
+
canRedo: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface GridContractStats {
|
|
154
|
+
/** Rows behind the grid. Null in server mode unless `rowCount` was given. */
|
|
155
|
+
totalRows: number | null;
|
|
156
|
+
/** Rows matching the current query. Null in server mode. */
|
|
157
|
+
filteredRows: number | null;
|
|
158
|
+
pageRows: number;
|
|
159
|
+
selectedRows: number;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface GridRuntimeContract {
|
|
163
|
+
contract: "gbs.datagrid";
|
|
164
|
+
contractVersion: string;
|
|
165
|
+
component: "DataGrid";
|
|
166
|
+
/** Stable for a given set of columns; pass your own to make it meaningful. */
|
|
167
|
+
instanceId: string;
|
|
168
|
+
capabilities: GridCapabilities;
|
|
169
|
+
columns: GridContractColumn[];
|
|
170
|
+
/** Operation names this instance offers, after capabilities and policy. */
|
|
171
|
+
operations: GridOperationName[];
|
|
172
|
+
state: GridContractState;
|
|
173
|
+
stats: GridContractStats;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --------------------------------------------------------------------- build
|
|
177
|
+
|
|
178
|
+
/** FNV-1a. Short, stable, and not a security boundary. */
|
|
179
|
+
function hash(text: string): string {
|
|
180
|
+
let h = 0x811c9dc5;
|
|
181
|
+
for (let i = 0; i < text.length; i++) {
|
|
182
|
+
h ^= text.charCodeAt(i);
|
|
183
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
184
|
+
}
|
|
185
|
+
return h.toString(36);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function summarise<T>(
|
|
189
|
+
column: ResolvedColumn<T>,
|
|
190
|
+
values: unknown[],
|
|
191
|
+
maxEnumValues: number,
|
|
192
|
+
): GridColumnStats | undefined {
|
|
193
|
+
const present = values.filter((v) => v !== null && v !== undefined && v !== "");
|
|
194
|
+
const nulls = values.length - present.length;
|
|
195
|
+
if (present.length === 0) return undefined;
|
|
196
|
+
|
|
197
|
+
switch (column.type) {
|
|
198
|
+
case "number": {
|
|
199
|
+
const numbers = present.map(Number).filter((n) => !Number.isNaN(n));
|
|
200
|
+
if (numbers.length === 0) return undefined;
|
|
201
|
+
return { kind: "number", min: Math.min(...numbers), max: Math.max(...numbers), nulls };
|
|
202
|
+
}
|
|
203
|
+
case "date": {
|
|
204
|
+
const times = present.map(toTime).filter((t): t is number => t !== null);
|
|
205
|
+
if (times.length === 0) return undefined;
|
|
206
|
+
// Local, not UTC: a date-only value is read as a local date, so
|
|
207
|
+
// formatting it back through UTC would shift it east of Greenwich.
|
|
208
|
+
return {
|
|
209
|
+
kind: "date",
|
|
210
|
+
min: toDateInputValue(Math.min(...times)),
|
|
211
|
+
max: toDateInputValue(Math.max(...times)),
|
|
212
|
+
nulls,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
case "boolean": {
|
|
216
|
+
let yes = 0;
|
|
217
|
+
for (const value of present) if (value) yes += 1;
|
|
218
|
+
return { kind: "boolean", true: yes, false: present.length - yes, nulls };
|
|
219
|
+
}
|
|
220
|
+
default: {
|
|
221
|
+
const distinct = new Set(present.map(String));
|
|
222
|
+
const stats: GridColumnStats = { kind: "string", distinct: distinct.size, nulls };
|
|
223
|
+
/*
|
|
224
|
+
* Category columns get their values listed, which is what stops a model
|
|
225
|
+
* inventing a region that does not exist. Identifier columns must not:
|
|
226
|
+
* a name or an invoice number is the data itself, and in a small table
|
|
227
|
+
* every column looks low-cardinality. The test is whether any value
|
|
228
|
+
* repeats — a category has fewer distinct values than it has rows, an
|
|
229
|
+
* identifier has exactly as many.
|
|
230
|
+
*/
|
|
231
|
+
if (distinct.size <= maxEnumValues && distinct.size < present.length) {
|
|
232
|
+
stats.values = [...distinct].sort();
|
|
233
|
+
}
|
|
234
|
+
return stats;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface BuildContractOptions<T> {
|
|
240
|
+
options: GridOptions<T>;
|
|
241
|
+
dataset: GridDataset<T>;
|
|
242
|
+
state: import("../core/types").GridState;
|
|
243
|
+
instanceId?: string;
|
|
244
|
+
semantics?: Readonly<Record<string, GridColumnSemantics>>;
|
|
245
|
+
policy?: GridAgentPolicy;
|
|
246
|
+
/** Column summaries and derived enum values. Default true. */
|
|
247
|
+
stats?: boolean;
|
|
248
|
+
/** Above this many distinct values a string column stops listing them. Default 20. */
|
|
249
|
+
maxEnumValues?: number;
|
|
250
|
+
canUndo?: boolean;
|
|
251
|
+
canRedo?: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function buildGridContract<T>(input: BuildContractOptions<T>): GridRuntimeContract {
|
|
255
|
+
const {
|
|
256
|
+
options,
|
|
257
|
+
dataset,
|
|
258
|
+
state,
|
|
259
|
+
semantics = {},
|
|
260
|
+
policy = {},
|
|
261
|
+
stats: wantStats = true,
|
|
262
|
+
maxEnumValues = 20,
|
|
263
|
+
canUndo = false,
|
|
264
|
+
canRedo = false,
|
|
265
|
+
} = input;
|
|
266
|
+
|
|
267
|
+
const denyPii = policy.denyPii !== false;
|
|
268
|
+
const denied = new Set(policy.deny ?? []);
|
|
269
|
+
const deniedFilter = new Set(policy.denyFilter ?? []);
|
|
270
|
+
|
|
271
|
+
const pagination = options.enablePagination !== false;
|
|
272
|
+
const selection = options.enableRowSelection !== undefined && options.enableRowSelection !== false;
|
|
273
|
+
const filtering = options.enableFiltering !== false;
|
|
274
|
+
|
|
275
|
+
const columns: GridContractColumn[] = dataset.columns.map((column) => {
|
|
276
|
+
const meta = semantics[column.id] ?? {};
|
|
277
|
+
const off = denied.has(column.id) || (denyPii && meta.pii === true);
|
|
278
|
+
const noFilter = off || deniedFilter.has(column.id);
|
|
279
|
+
const visible = state.columnVisibility[column.id] !== false;
|
|
280
|
+
const pinned = state.columnPinning.left.includes(column.id)
|
|
281
|
+
? "left"
|
|
282
|
+
: state.columnPinning.right.includes(column.id)
|
|
283
|
+
? "right"
|
|
284
|
+
: null;
|
|
285
|
+
|
|
286
|
+
const filterable = column.filterable && filtering && !noFilter;
|
|
287
|
+
const entry: GridContractColumn = {
|
|
288
|
+
id: column.id,
|
|
289
|
+
label: column.header,
|
|
290
|
+
type: column.type,
|
|
291
|
+
visible,
|
|
292
|
+
pinned,
|
|
293
|
+
sortable: column.sortable && options.enableSorting !== false && !off,
|
|
294
|
+
filterable,
|
|
295
|
+
searchable: column.searchable && filtering && !noFilter,
|
|
296
|
+
hideable: column.hideable && options.enableColumnHiding !== false,
|
|
297
|
+
pinnable: column.pinnable && options.enableColumnPinning !== false,
|
|
298
|
+
reorderable: column.reorderable && options.enableColumnReordering !== false,
|
|
299
|
+
resizable: column.resizable && options.enableColumnResizing !== false,
|
|
300
|
+
exportable: column.exportable && !off,
|
|
301
|
+
editable: column.def.editable !== undefined && column.def.editable !== false,
|
|
302
|
+
operators: filterable ? getFilterOperators(column) : [],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (column.def.options) entry.options = [...column.def.options];
|
|
306
|
+
if (meta.unit) entry.unit = meta.unit;
|
|
307
|
+
if (meta.percentBasis) entry.percentBasis = meta.percentBasis;
|
|
308
|
+
if (meta.higherIsBetter !== undefined) entry.higherIsBetter = meta.higherIsBetter;
|
|
309
|
+
if (meta.synonyms?.length) entry.synonyms = [...meta.synonyms];
|
|
310
|
+
if (meta.description) entry.description = meta.description;
|
|
311
|
+
if (off) entry.restricted = "policy";
|
|
312
|
+
|
|
313
|
+
// A restricted column is named so a caller knows why it cannot be used,
|
|
314
|
+
// and summarised never, because a summary is the data in miniature.
|
|
315
|
+
if (wantStats && !off && !entry.options) {
|
|
316
|
+
const summary = summarise(column, dataset.values(column.id), maxEnumValues);
|
|
317
|
+
if (summary) entry.stats = summary;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return entry;
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const capabilities: GridCapabilities = {
|
|
324
|
+
mode: options.mode ?? "client",
|
|
325
|
+
sorting: options.enableSorting !== false,
|
|
326
|
+
multiSort: options.enableSorting !== false && options.enableMultiSort !== false,
|
|
327
|
+
filtering,
|
|
328
|
+
globalSearch: filtering,
|
|
329
|
+
pagination,
|
|
330
|
+
selection,
|
|
331
|
+
selectionMode: options.selectionMode ?? "multiple",
|
|
332
|
+
columnResizing: options.enableColumnResizing !== false,
|
|
333
|
+
columnReordering: options.enableColumnReordering !== false,
|
|
334
|
+
columnPinning: options.enableColumnPinning !== false,
|
|
335
|
+
columnHiding: options.enableColumnHiding !== false,
|
|
336
|
+
editing: columns.some((column) => column.editable) && options.onCellEdit !== undefined,
|
|
337
|
+
exportFormats: [...(policy.allowExportFormats ?? ["csv", "excel", "pdf"])],
|
|
338
|
+
print: true,
|
|
339
|
+
clipboard: true,
|
|
340
|
+
undo: true,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const available: Record<NonNullable<import("./operations").GridOperationDefinition["requires"]>, boolean> = {
|
|
344
|
+
sorting: capabilities.sorting,
|
|
345
|
+
filtering: capabilities.filtering,
|
|
346
|
+
selection: capabilities.selection,
|
|
347
|
+
pagination: capabilities.pagination,
|
|
348
|
+
columnHiding: capabilities.columnHiding,
|
|
349
|
+
columnPinning: capabilities.columnPinning,
|
|
350
|
+
columnReordering: capabilities.columnReordering,
|
|
351
|
+
columnResizing: capabilities.columnResizing,
|
|
352
|
+
};
|
|
353
|
+
const withheld = new Set(policy.denyOperations ?? []);
|
|
354
|
+
if (capabilities.exportFormats.length === 0) withheld.add("export");
|
|
355
|
+
|
|
356
|
+
const operations = (Object.values(GRID_OPERATIONS) as (typeof GRID_OPERATIONS)[GridOperationName][])
|
|
357
|
+
.filter((op) => !withheld.has(op.name) && (op.requires === undefined || available[op.requires]))
|
|
358
|
+
.map((op) => op.name);
|
|
359
|
+
|
|
360
|
+
const filteredRows = dataset.count(state.filters, state.globalFilter);
|
|
361
|
+
const totalRows = dataset.total;
|
|
362
|
+
const pageSize = Math.max(1, state.pagination.pageSize);
|
|
363
|
+
const effectiveTotal = filteredRows ?? totalRows ?? dataset.rows.length;
|
|
364
|
+
const pageCount = pagination ? Math.max(1, Math.ceil(effectiveTotal / pageSize)) : 1;
|
|
365
|
+
const pageIndex = Math.min(Math.max(0, state.pagination.pageIndex), pageCount - 1);
|
|
366
|
+
const selectedRows = Object.values(state.rowSelection).filter(Boolean).length;
|
|
367
|
+
|
|
368
|
+
const contractState: GridContractState = {
|
|
369
|
+
sorting: state.sorting.map((item) => ({
|
|
370
|
+
column: item.columnId,
|
|
371
|
+
direction: item.desc ? "desc" : "asc",
|
|
372
|
+
})),
|
|
373
|
+
filters: state.filters.map((filter) => ({
|
|
374
|
+
column: filter.columnId,
|
|
375
|
+
operator: filter.operator,
|
|
376
|
+
...(filter.value !== undefined ? { value: filter.value } : {}),
|
|
377
|
+
...(filter.value2 !== undefined ? { value2: filter.value2 } : {}),
|
|
378
|
+
})),
|
|
379
|
+
search: state.globalFilter,
|
|
380
|
+
page: { index: pageIndex, size: pageSize, count: pageCount },
|
|
381
|
+
selection: {
|
|
382
|
+
count: selectedRows,
|
|
383
|
+
all: filteredRows !== null && filteredRows > 0 && selectedRows >= filteredRows,
|
|
384
|
+
},
|
|
385
|
+
density: state.density,
|
|
386
|
+
hiddenColumns: columns.filter((column) => !column.visible).map((column) => column.id),
|
|
387
|
+
pinnedColumns: { left: [...state.columnPinning.left], right: [...state.columnPinning.right] },
|
|
388
|
+
canUndo,
|
|
389
|
+
canRedo,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
contract: "gbs.datagrid",
|
|
394
|
+
contractVersion: GRID_CONTRACT_VERSION,
|
|
395
|
+
component: "DataGrid",
|
|
396
|
+
instanceId: input.instanceId ?? `dg-${hash(dataset.columns.map((c) => c.id).join("|"))}`,
|
|
397
|
+
capabilities,
|
|
398
|
+
columns,
|
|
399
|
+
operations,
|
|
400
|
+
state: contractState,
|
|
401
|
+
stats: {
|
|
402
|
+
totalRows,
|
|
403
|
+
filteredRows,
|
|
404
|
+
pageRows: pagination
|
|
405
|
+
? Math.min(pageSize, Math.max(0, effectiveTotal - pageIndex * pageSize))
|
|
406
|
+
: effectiveTotal,
|
|
407
|
+
selectedRows,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The rows and columns the agent layer is allowed to look at.
|
|
3
|
+
*
|
|
4
|
+
* Everything else in `agent/` goes through this: it is the one place that
|
|
5
|
+
* touches the data, which makes the privacy rule easy to hold — the runtime
|
|
6
|
+
* contract reports counts and bounded summaries from here, never rows.
|
|
7
|
+
*
|
|
8
|
+
* The grid's own `resolveColumns`, `filterRows` and `paginate` are reused
|
|
9
|
+
* rather than reimplemented, so a dry run counts exactly the rows the grid
|
|
10
|
+
* would show.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { resolveColumns } from "../core/columns";
|
|
14
|
+
import { filterRows } from "../core/filtering";
|
|
15
|
+
import { buildRows, createRowIdGetter } from "../core/rows";
|
|
16
|
+
import type {
|
|
17
|
+
ColumnFilter,
|
|
18
|
+
GridOptions,
|
|
19
|
+
GridRow,
|
|
20
|
+
ResolvedColumn,
|
|
21
|
+
RowId,
|
|
22
|
+
} from "../core/types";
|
|
23
|
+
import { createFormatters, type Formatters } from "../core/values";
|
|
24
|
+
|
|
25
|
+
export interface GridDataset<T> {
|
|
26
|
+
columns: ResolvedColumn<T>[];
|
|
27
|
+
columnById: Map<string, ResolvedColumn<T>>;
|
|
28
|
+
rows: GridRow<T>[];
|
|
29
|
+
formatters: Formatters;
|
|
30
|
+
/** True in server mode, where `rows` is only the page the server sent. */
|
|
31
|
+
partial: boolean;
|
|
32
|
+
/** Total rows behind the grid, or null when only the server knows. */
|
|
33
|
+
total: number | null;
|
|
34
|
+
hasRowId(id: RowId): boolean;
|
|
35
|
+
/** How many rows a query would show. Null when the data is only a page. */
|
|
36
|
+
count(filters: readonly ColumnFilter[], globalFilter: string): number | null;
|
|
37
|
+
/** Raw values of one column, for summary statistics. */
|
|
38
|
+
values(columnId: string): unknown[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const keyOf = (filters: readonly ColumnFilter[], globalFilter: string) =>
|
|
42
|
+
`${globalFilter}\u0000${JSON.stringify(filters)}`;
|
|
43
|
+
|
|
44
|
+
export function createDataset<T>(options: GridOptions<T>, locale?: string): GridDataset<T> {
|
|
45
|
+
const columns = resolveColumns(options.columns);
|
|
46
|
+
const columnById = new Map(columns.map((column) => [column.id, column]));
|
|
47
|
+
const rows = buildRows(options.data, createRowIdGetter(options.getRowId));
|
|
48
|
+
const formatters = createFormatters(locale);
|
|
49
|
+
const partial = options.mode === "server";
|
|
50
|
+
|
|
51
|
+
let ids: Set<RowId> | null = null;
|
|
52
|
+
const counts = new Map<string, number>();
|
|
53
|
+
const valueCache = new Map<string, unknown[]>();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
columns,
|
|
57
|
+
columnById,
|
|
58
|
+
rows,
|
|
59
|
+
formatters,
|
|
60
|
+
partial,
|
|
61
|
+
total: partial ? (options.rowCount ?? null) : rows.length,
|
|
62
|
+
|
|
63
|
+
hasRowId(id) {
|
|
64
|
+
ids ??= new Set(rows.map((row) => row.id));
|
|
65
|
+
return ids.has(id);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
count(filters, globalFilter) {
|
|
69
|
+
// In server mode the client holds one page, so any count would be a lie.
|
|
70
|
+
if (partial) return null;
|
|
71
|
+
const key = keyOf(filters, globalFilter);
|
|
72
|
+
let hit = counts.get(key);
|
|
73
|
+
if (hit === undefined) {
|
|
74
|
+
hit = filterRows(rows, columns, filters, globalFilter, formatters).length;
|
|
75
|
+
// A handful of distinct queries per interaction; this is a guard, not a cache policy.
|
|
76
|
+
if (counts.size > 64) counts.clear();
|
|
77
|
+
counts.set(key, hit);
|
|
78
|
+
}
|
|
79
|
+
return hit;
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
values(columnId) {
|
|
83
|
+
let hit = valueCache.get(columnId);
|
|
84
|
+
if (!hit) {
|
|
85
|
+
const column = columnById.get(columnId);
|
|
86
|
+
hit = column ? rows.map((row) => column.getValue(row.original)) : [];
|
|
87
|
+
valueCache.set(columnId, hit);
|
|
88
|
+
}
|
|
89
|
+
return hit;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|