gbs-add-block 1.2.7 → 1.2.9

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.
Files changed (57) hide show
  1. package/README.md +3 -3
  2. package/index.cjs +10 -9
  3. package/package.json +1 -1
  4. package/source/components/combobox/README.md +170 -0
  5. package/source/components/combobox/__tests__/core.test.ts +134 -0
  6. package/source/components/combobox/core/filter.ts +155 -0
  7. package/source/components/combobox/core/index.ts +16 -0
  8. package/source/components/combobox/core/types.ts +48 -0
  9. package/source/components/combobox/core/virtual.ts +67 -0
  10. package/source/components/combobox/index.ts +13 -0
  11. package/source/components/combobox/react/Listbox.tsx +302 -0
  12. package/source/components/combobox/react/MultiSelect.tsx +263 -0
  13. package/source/components/combobox/react/Popover.tsx +70 -0
  14. package/source/components/combobox/react/Select.tsx +198 -0
  15. package/source/components/combobox/react/icons.tsx +34 -0
  16. package/source/components/combobox/react/locale.ts +16 -0
  17. package/source/components/combobox/react/props.ts +86 -0
  18. package/source/components/combobox/react/useCombobox.ts +399 -0
  19. package/source/components/combobox/styles.css +417 -0
  20. package/source/components/datagridbeta/README.md +232 -0
  21. package/source/components/datagridbeta/__tests__/core.test.ts +341 -0
  22. package/source/components/datagridbeta/__tests__/export.test.ts +70 -0
  23. package/source/components/datagridbeta/core/columnHelper.ts +21 -0
  24. package/source/components/datagridbeta/core/columns.ts +288 -0
  25. package/source/components/datagridbeta/core/filtering.ts +237 -0
  26. package/source/components/datagridbeta/core/grid.ts +616 -0
  27. package/source/components/datagridbeta/core/index.ts +14 -0
  28. package/source/components/datagridbeta/core/rows.ts +64 -0
  29. package/source/components/datagridbeta/core/sorting.ts +111 -0
  30. package/source/components/datagridbeta/core/state.ts +82 -0
  31. package/source/components/datagridbeta/core/store.ts +52 -0
  32. package/source/components/datagridbeta/core/types.ts +266 -0
  33. package/source/components/datagridbeta/core/values.ts +98 -0
  34. package/source/components/datagridbeta/core/virtual.ts +85 -0
  35. package/source/components/datagridbeta/export/csv.ts +32 -0
  36. package/source/components/datagridbeta/export/download.ts +12 -0
  37. package/source/components/datagridbeta/export/pdf.ts +61 -0
  38. package/source/components/datagridbeta/export/table.ts +73 -0
  39. package/source/components/datagridbeta/export/xlsx.ts +145 -0
  40. package/source/components/datagridbeta/export/zip.ts +87 -0
  41. package/source/components/datagridbeta/index.ts +7 -0
  42. package/source/components/datagridbeta/react/Cell.tsx +282 -0
  43. package/source/components/datagridbeta/react/ColumnMenu.tsx +135 -0
  44. package/source/components/datagridbeta/react/DataGrid.tsx +296 -0
  45. package/source/components/datagridbeta/react/FilterForm.tsx +142 -0
  46. package/source/components/datagridbeta/react/HeaderRow.tsx +306 -0
  47. package/source/components/datagridbeta/react/Pagination.tsx +107 -0
  48. package/source/components/datagridbeta/react/Popover.tsx +79 -0
  49. package/source/components/datagridbeta/react/Row.tsx +85 -0
  50. package/source/components/datagridbeta/react/Toolbar.tsx +266 -0
  51. package/source/components/datagridbeta/react/Viewport.tsx +167 -0
  52. package/source/components/datagridbeta/react/context.ts +57 -0
  53. package/source/components/datagridbeta/react/hooks.ts +80 -0
  54. package/source/components/datagridbeta/react/icons.tsx +72 -0
  55. package/source/components/datagridbeta/react/keyboard.ts +110 -0
  56. package/source/components/datagridbeta/react/locale.ts +128 -0
  57. package/source/components/datagridbeta/styles.css +717 -0
@@ -0,0 +1,341 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { computeLayout, getLayoutVars, moveColumn, resolveColumns } from "../core/columns";
3
+ import { filterRows } from "../core/filtering";
4
+ import { createGridEngine, type GridModel } from "../core/grid";
5
+ import { buildRows, createRowIdGetter, paginate } from "../core/rows";
6
+ import { sortRows, toggleSorting } from "../core/sorting";
7
+ import { createInitialState, mergeState } from "../core/state";
8
+ import type { ColumnDef, ColumnFilter, GridOptions, GridRow, SortItem } from "../core/types";
9
+ import { createFormatters } from "../core/values";
10
+ import { getColumnRange, getRowRange } from "../core/virtual";
11
+
12
+ interface Person {
13
+ id: number;
14
+ name: string;
15
+ age: number | null;
16
+ city: string | null;
17
+ joined: string;
18
+ active: boolean;
19
+ status: "new" | "active" | "gone";
20
+ }
21
+
22
+ const people: Person[] = [
23
+ { id: 1, name: "Alice", age: 34, city: "Paris", joined: "2024-01-15", active: true, status: "active" },
24
+ { id: 2, name: "bob", age: null, city: "Berlin", joined: "2023-06-01", active: false, status: "new" },
25
+ { id: 3, name: "Carol", age: 28, city: null, joined: "2024-01-16", active: true, status: "gone" },
26
+ { id: 4, name: "Dave", age: 41, city: "Paris", joined: "2022-11-30", active: false, status: "active" },
27
+ { id: 5, name: "Item10", age: 28, city: "Rome", joined: "2024-03-01", active: true, status: "new" },
28
+ { id: 6, name: "Item2", age: 50, city: "Berlin", joined: "2021-07-07", active: true, status: "active" },
29
+ ];
30
+
31
+ const defs: ColumnDef<Person>[] = [
32
+ { field: "id", type: "number" },
33
+ { field: "name" },
34
+ { field: "age", type: "number" },
35
+ { field: "city" },
36
+ { field: "joined", type: "date" },
37
+ { field: "active", type: "boolean" },
38
+ {
39
+ field: "status",
40
+ options: [
41
+ { label: "New", value: "new" },
42
+ { label: "Active", value: "active" },
43
+ { label: "Gone", value: "gone" },
44
+ ],
45
+ },
46
+ ];
47
+
48
+ const columns = resolveColumns(defs);
49
+ const formatters = createFormatters("en-US");
50
+ const rows = buildRows(people, createRowIdGetter<Person>("id"));
51
+ const ids = (list: GridRow<Person>[]) => list.map((row) => row.original.id);
52
+
53
+ describe("columns", () => {
54
+ it("derives ids and readable headers", () => {
55
+ expect(columns[0].id).toBe("id");
56
+ expect(columns[1].header).toBe("Name");
57
+ expect(resolveColumns<{ firstName: string }>([{ field: "firstName" }])[0].header).toBe("First Name");
58
+ expect(resolveColumns<{ start_date: string }>([{ field: "start_date" }])[0].header).toBe("Start date");
59
+ });
60
+
61
+ it("rejects duplicate ids", () => {
62
+ expect(() => resolveColumns<Person>([{ field: "name" }, { field: "name" }])).toThrow(/Duplicate/);
63
+ });
64
+
65
+ it("lays out pinned, ordered, sized and hidden columns", () => {
66
+ const layout = computeLayout(columns, {
67
+ columnOrder: ["city", "name"],
68
+ columnVisibility: { age: false },
69
+ columnSizing: { name: 200 },
70
+ columnPinning: { left: ["id"], right: ["status"] },
71
+ });
72
+ expect(layout.left.map((i) => i.column.id)).toEqual(["id"]);
73
+ expect(layout.center.map((i) => i.column.id)).toEqual(["city", "name", "joined", "active"]);
74
+ expect(layout.right.map((i) => i.column.id)).toEqual(["status"]);
75
+ expect(layout.center[1]).toMatchObject({ offset: 160, width: 200 });
76
+ expect(layout.left[0].pinEdge).toBe(true);
77
+ expect(layout.right[0].pinEdge).toBe(true);
78
+ expect(layout.totalWidth).toBe(160 * 5 + 200);
79
+ });
80
+
81
+ it("offsets right-pinned columns from the end, including resize previews", () => {
82
+ const layout = computeLayout(columns, {
83
+ columnOrder: [],
84
+ columnVisibility: {},
85
+ columnSizing: {},
86
+ columnPinning: { left: [], right: ["city", "status"] },
87
+ });
88
+ const [city, status] = layout.right;
89
+ expect(getLayoutVars(layout)[`--dg-c${status.visibleIndex}-o`]).toBe("0px");
90
+ expect(getLayoutVars(layout)[`--dg-c${city.visibleIndex}-o`]).toBe("160px");
91
+ expect(getLayoutVars(layout, { status: 300 })[`--dg-c${city.visibleIndex}-o`]).toBe("300px");
92
+ });
93
+
94
+ it("moves a column into the target's pin section and back", () => {
95
+ const pinned = moveColumn(columns, { columnOrder: [], columnPinning: { left: ["id"], right: [] } }, "city", "id", "after");
96
+ expect(pinned.columnPinning.left).toEqual(["id", "city"]);
97
+
98
+ const unpinned = moveColumn(columns, pinned, "city", "name", "before");
99
+ expect(unpinned.columnPinning.left).toEqual(["id"]);
100
+ expect(unpinned.columnOrder.indexOf("city")).toBe(unpinned.columnOrder.indexOf("name") - 1);
101
+ });
102
+ });
103
+
104
+ describe("filtering", () => {
105
+ const run = (filters: ColumnFilter[], search = "") =>
106
+ ids(filterRows(rows, columns, filters, search, formatters));
107
+
108
+ it("returns the input array when nothing filters", () => {
109
+ expect(filterRows(rows, columns, [], " ", formatters)).toBe(rows);
110
+ expect(filterRows(rows, columns, [{ columnId: "name", operator: "contains", value: "" }], "", formatters)).toBe(rows);
111
+ });
112
+
113
+ it("matches text case-insensitively", () => {
114
+ expect(run([{ columnId: "name", operator: "contains", value: "B" }])).toEqual([2]);
115
+ });
116
+
117
+ it("applies every column filter together", () => {
118
+ expect(
119
+ run([
120
+ { columnId: "city", operator: "equals", value: "paris" },
121
+ { columnId: "active", operator: "equals", value: false },
122
+ ]),
123
+ ).toEqual([4]);
124
+ });
125
+
126
+ it("handles empty cell values", () => {
127
+ expect(run([{ columnId: "city", operator: "startsWith", value: "p" }])).toEqual([1, 4]);
128
+ expect(run([{ columnId: "age", operator: "isEmpty" }])).toEqual([2]);
129
+ });
130
+
131
+ it("filters numbers", () => {
132
+ expect(run([{ columnId: "age", operator: "between", value: 28, value2: 34 }])).toEqual([1, 3, 5]);
133
+ expect(run([{ columnId: "age", operator: "gt", value: 40 }])).toEqual([4, 6]);
134
+ });
135
+
136
+ it("filters dates by local calendar day", () => {
137
+ expect(run([{ columnId: "joined", operator: "equals", value: "2024-01-15" }])).toEqual([1]);
138
+ expect(run([{ columnId: "joined", operator: "before", value: "2023-01-01" }])).toEqual([4, 6]);
139
+ expect(run([{ columnId: "joined", operator: "after", value: "2024-01-15" }])).toEqual([3, 5]);
140
+ expect(run([{ columnId: "joined", operator: "between", value: "2024-01-15", value2: "2024-01-16" }])).toEqual([1, 3]);
141
+ });
142
+
143
+ it("filters option sets", () => {
144
+ expect(run([{ columnId: "status", operator: "in", value: ["new", "gone"] }])).toEqual([2, 3, 5]);
145
+ });
146
+
147
+ it("searches display values and requires every term", () => {
148
+ expect(run([], "paris active")).toEqual([1, 4]);
149
+ expect(run([], "Gone")).toEqual([3]);
150
+ });
151
+ });
152
+
153
+ describe("sorting", () => {
154
+ const sorted = (sorting: SortItem[]) => ids(sortRows(rows, columns, sorting));
155
+
156
+ it("sorts numbers with empty values last in both directions", () => {
157
+ expect(sorted([{ columnId: "age", desc: false }])).toEqual([3, 5, 1, 4, 6, 2]);
158
+ expect(sorted([{ columnId: "age", desc: true }])).toEqual([6, 4, 1, 3, 5, 2]);
159
+ });
160
+
161
+ it("uses natural, case-insensitive string order", () => {
162
+ expect(sorted([{ columnId: "name", desc: false }])).toEqual([1, 2, 3, 4, 6, 5]);
163
+ });
164
+
165
+ it("applies sorts in priority order", () => {
166
+ expect(
167
+ sorted([
168
+ { columnId: "city", desc: false },
169
+ { columnId: "age", desc: true },
170
+ ]),
171
+ ).toEqual([6, 2, 4, 1, 5, 3]);
172
+ });
173
+
174
+ it("sorts dates and option labels", () => {
175
+ expect(sorted([{ columnId: "joined", desc: false }])).toEqual([6, 4, 2, 1, 3, 5]);
176
+ expect(sorted([{ columnId: "status", desc: false }])).toEqual([1, 4, 6, 3, 2, 5]);
177
+ });
178
+
179
+ it("cycles ascending, descending, off", () => {
180
+ const asc = toggleSorting([], "a", false);
181
+ expect(asc).toEqual([{ columnId: "a", desc: false }]);
182
+ const desc = toggleSorting(asc, "a", false);
183
+ expect(desc).toEqual([{ columnId: "a", desc: true }]);
184
+ expect(toggleSorting(desc, "a", false)).toEqual([]);
185
+ expect(toggleSorting(desc, "b", true)).toEqual([
186
+ { columnId: "a", desc: true },
187
+ { columnId: "b", desc: false },
188
+ ]);
189
+ expect(toggleSorting(desc, "b", false)).toEqual([{ columnId: "b", desc: false }]);
190
+ });
191
+ });
192
+
193
+ describe("pagination and virtualization", () => {
194
+ it("clamps the page index", () => {
195
+ const page = paginate(rows, { pageIndex: 9, pageSize: 4 }, { enabled: true, server: false });
196
+ expect(page).toMatchObject({ pageIndex: 1, pageCount: 2, pageOffset: 4 });
197
+ expect(ids(page.rows)).toEqual([5, 6]);
198
+ });
199
+
200
+ it("uses the server row count", () => {
201
+ const page = paginate(rows.slice(0, 2), { pageIndex: 3, pageSize: 2 }, { enabled: true, server: true, rowCount: 101 });
202
+ expect(page).toMatchObject({ pageCount: 51, pageOffset: 6, rowCount: 101 });
203
+ expect(page.rows).toHaveLength(2);
204
+ });
205
+
206
+ it("computes the visible row window", () => {
207
+ const viewport = { scrollTop: 400, scrollLeft: 0, width: 800, height: 440 };
208
+ expect(getRowRange(viewport, 1000, 40, 40, 2)).toEqual({ start: 8, end: 22 });
209
+ });
210
+
211
+ it("computes the visible column window", () => {
212
+ const many = resolveColumns<Record<string, number>>(
213
+ Array.from({ length: 50 }, (_, i) => ({ id: `c${i}`, accessor: (row) => row[`c${i}`], width: 100 })),
214
+ );
215
+ const layout = computeLayout(many, {
216
+ columnOrder: [],
217
+ columnVisibility: {},
218
+ columnSizing: {},
219
+ columnPinning: { left: ["c0"], right: [] },
220
+ });
221
+ expect(getColumnRange(layout, { scrollTop: 0, scrollLeft: 1000, width: 500, height: 300 }, 0)).toEqual({ start: 10, end: 14 });
222
+ expect(getColumnRange(layout, { scrollTop: 0, scrollLeft: 0, width: 0, height: 0 })).toEqual({ start: 0, end: 49 });
223
+ });
224
+ });
225
+
226
+ describe("state", () => {
227
+ it("seeds pinning and visibility from column definitions", () => {
228
+ const state = createInitialState<Person>({
229
+ data: [],
230
+ columns: [{ field: "id", pin: "left" }, { field: "name", hidden: true }],
231
+ });
232
+ expect(state.columnPinning).toEqual({ left: ["id"], right: [] });
233
+ expect(state.columnVisibility).toEqual({ name: false });
234
+ });
235
+
236
+ it("prefers defined controlled values", () => {
237
+ const state = createInitialState<Person>({ data: [], columns: [] });
238
+ expect(mergeState(state, undefined)).toBe(state);
239
+ expect(mergeState(state, { globalFilter: undefined })).toBe(state);
240
+ expect(mergeState(state, { globalFilter: "x" }).globalFilter).toBe("x");
241
+ });
242
+ });
243
+
244
+ function setup(overrides: Partial<GridOptions<Person>> = {}) {
245
+ const options: GridOptions<Person> = {
246
+ data: people,
247
+ columns: defs,
248
+ getRowId: "id",
249
+ enableRowSelection: true,
250
+ ...overrides,
251
+ };
252
+ const engine = createGridEngine(options);
253
+ const resolved = resolveColumns(options.columns);
254
+ const state = engine.api.getState();
255
+ const filtered = filterRows(rows, resolved, state.filters, state.globalFilter, formatters);
256
+ const sortedRows = sortRows(filtered, resolved, state.sorting);
257
+ const model: GridModel<Person> = {
258
+ columns: resolved,
259
+ coreRows: rows,
260
+ sortedRows,
261
+ page: paginate(sortedRows, state.pagination, { enabled: true, server: false }),
262
+ layout: computeLayout(resolved, state),
263
+ rowHeight: 40,
264
+ headerHeight: 40,
265
+ formatters,
266
+ };
267
+ engine.sync(options, model);
268
+ return engine;
269
+ }
270
+
271
+ describe("engine", () => {
272
+ it("returns to the first page and reports the query when filters change", () => {
273
+ const onQueryChange = vi.fn();
274
+ const engine = setup({ onQueryChange, initialState: { pagination: { pageIndex: 2, pageSize: 2 } } });
275
+
276
+ engine.api.setFilter("city", { operator: "equals", value: "paris" });
277
+
278
+ expect(engine.api.getState().pagination.pageIndex).toBe(0);
279
+ expect(onQueryChange).toHaveBeenCalledWith(
280
+ expect.objectContaining({
281
+ filters: [{ columnId: "city", operator: "equals", value: "paris" }],
282
+ pagination: { pageIndex: 0, pageSize: 2 },
283
+ }),
284
+ );
285
+ });
286
+
287
+ it("leaves controlled state to the parent", () => {
288
+ const onStateChange = vi.fn();
289
+ const engine = setup({ state: { sorting: [] }, onStateChange });
290
+
291
+ engine.api.toggleSort("name");
292
+
293
+ expect(engine.api.getState().sorting).toEqual([]);
294
+ expect(onStateChange.mock.calls[0][0].sorting).toEqual([{ columnId: "name", desc: false }]);
295
+ });
296
+
297
+ it("selects ranges and skips rows that cannot be selected", () => {
298
+ const engine = setup({ enableRowSelection: (row) => row.id !== 3 });
299
+
300
+ engine.api.toggleRowSelected("1");
301
+ engine.api.toggleRowSelected("4", { range: true });
302
+ expect(engine.api.getSelectedRowIds()).toEqual(["1", "2", "4"]);
303
+
304
+ engine.api.toggleAllRowsSelected(true);
305
+ expect(engine.api.getSelectedRows().map((p) => p.id)).toEqual([1, 2, 4, 5, 6]);
306
+ });
307
+
308
+ it("keeps one row selected in single mode", () => {
309
+ const engine = setup({ selectionMode: "single" });
310
+ engine.api.toggleRowSelected("1");
311
+ engine.api.toggleRowSelected("2");
312
+ expect(engine.api.getSelectedRowIds()).toEqual(["2"]);
313
+ });
314
+
315
+ it("validates edits and tracks async saves", async () => {
316
+ let finishSave!: () => void;
317
+ const onCellEdit = vi.fn(() => new Promise<void>((resolve) => (finishSave = resolve)));
318
+ const engine = setup({
319
+ columns: [
320
+ { field: "id", type: "number" },
321
+ { field: "name", editable: true, validate: (value) => (String(value).trim() ? null : "Required") },
322
+ ],
323
+ onCellEdit,
324
+ });
325
+
326
+ engine.api.startEditing("1", "name");
327
+ expect(engine.store.getSnapshot().ui.editing).toEqual({ rowId: "1", columnId: "name" });
328
+
329
+ expect(engine.commitEdit("1", "name", " ")).toBe("Required");
330
+ expect(engine.commitEdit("1", "name", "Alicia")).toBeNull();
331
+ expect(onCellEdit).toHaveBeenCalledWith(
332
+ expect.objectContaining({ rowId: "1", columnId: "name", value: "Alicia", previousValue: "Alice" }),
333
+ );
334
+ expect(engine.store.getSnapshot().ui).toMatchObject({ editing: null, pending: { "1": { name: "Alicia" } } });
335
+
336
+ finishSave();
337
+ await Promise.resolve();
338
+ await Promise.resolve();
339
+ expect(engine.store.getSnapshot().ui.pending).toEqual({});
340
+ });
341
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { computeLayout, resolveColumns } from "../core/columns";
3
+ import { createInitialState } from "../core/state";
4
+ import type { ColumnDef } from "../core/types";
5
+ import { createFormatters } from "../core/values";
6
+ import { toCsv } from "../export/csv";
7
+ import { buildPrintHtml } from "../export/pdf";
8
+ import { buildExportTable } from "../export/table";
9
+ import { columnName, createXlsxBlob, escapeXml } from "../export/xlsx";
10
+ import { crc32 } from "../export/zip";
11
+
12
+ interface Person {
13
+ id: number;
14
+ name: string;
15
+ age: number | null;
16
+ joined: string;
17
+ active: boolean;
18
+ status: string;
19
+ }
20
+
21
+ const defs: ColumnDef<Person>[] = [
22
+ { field: "id", type: "number" },
23
+ { field: "name" },
24
+ { field: "age", type: "number" },
25
+ { field: "joined", type: "date" },
26
+ { field: "active", type: "boolean" },
27
+ { field: "status", options: [{ label: "Active", value: "active" }, { label: "New", value: "new" }] },
28
+ ];
29
+
30
+ const people: Person[] = [
31
+ { id: 1, name: '=HYPERLINK("x"), "quoted"', age: 34, joined: "2024-01-15", active: true, status: "active" },
32
+ { id: 2, name: "bob", age: null, joined: "2023-06-01", active: false, status: "new" },
33
+ ];
34
+
35
+ const columns = resolveColumns(defs);
36
+ const layout = computeLayout(columns, createInitialState({ data: people, columns: defs }));
37
+ const table = buildExportTable(people, layout.columns, createFormatters("en-US"));
38
+
39
+ describe("export", () => {
40
+ it("escapes CSV fields and neutralizes formulas", () => {
41
+ const [header, first, second] = toCsv(table).split("\r\n");
42
+ expect(header).toBe("Id,Name,Age,Joined,Active,Status");
43
+ expect(first).toBe(`1,"'=HYPERLINK(""x""), ""quoted""",34,"Jan 15, 2024",Yes,Active`);
44
+ expect(second).toBe(`2,bob,,"Jun 1, 2023",No,New`);
45
+ });
46
+
47
+ it("writes a typed, escaped XLSX package", async () => {
48
+ const bytes = new Uint8Array(await createXlsxBlob(table, "People & Co").arrayBuffer());
49
+ expect(String.fromCharCode(...bytes.slice(0, 4))).toBe("PK\x03\x04");
50
+
51
+ const text = new TextDecoder().decode(bytes);
52
+ expect(text).toContain("&quot;quoted&quot;");
53
+ expect(text).toContain('<c r="C2"><v>34</v></c>');
54
+ expect(text).toContain('<c r="E3" t="b"><v>0</v></c>');
55
+ expect(text).toContain('<c r="D2" s="2">');
56
+ expect(text).toContain('name="People &amp; Co"');
57
+ });
58
+
59
+ it("names columns, escapes XML and checksums like the spec", () => {
60
+ expect([0, 25, 26, 701, 702].map(columnName)).toEqual(["A", "Z", "AA", "ZZ", "AAA"]);
61
+ expect(escapeXml("a<b>&")).toBe("a&lt;b&gt;&amp;");
62
+ expect(crc32([new TextEncoder().encode("hello")])).toBe(0x3610a686);
63
+ });
64
+
65
+ it("escapes HTML in the print view", () => {
66
+ const html = buildPrintHtml(table, { title: "<Report>", orientation: "landscape", paperSize: "A4" });
67
+ expect(html).toContain("&#60;Report&#62;");
68
+ expect(html).not.toContain("<Report>");
69
+ });
70
+ });
@@ -0,0 +1,21 @@
1
+ import type { ColumnDef } from "./types";
2
+
3
+ type Base<T, V> = Omit<ColumnDef<T, V>, "id" | "field" | "accessor">;
4
+
5
+ /**
6
+ * Typed column builders: `value` in `cell`, `format`, `validate` and friends is
7
+ * inferred from the field or accessor.
8
+ */
9
+ export function createColumnHelper<T>() {
10
+ return {
11
+ field<K extends Extract<keyof T, string>>(field: K, def: Base<T, T[K]> & { id?: string } = {}): ColumnDef<T> {
12
+ return { ...def, field } as unknown as ColumnDef<T>;
13
+ },
14
+ accessor<V>(id: string, accessor: (row: T) => V, def: Base<T, V> = {}): ColumnDef<T> {
15
+ return { ...def, id, accessor } as unknown as ColumnDef<T>;
16
+ },
17
+ display(id: string, def: Base<T, undefined>): ColumnDef<T> {
18
+ return { ...def, id } as unknown as ColumnDef<T>;
19
+ },
20
+ };
21
+ }