gbs-add-block 1.2.7 → 1.2.8

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 (41) hide show
  1. package/README.md +3 -3
  2. package/index.cjs +9 -9
  3. package/package.json +1 -1
  4. package/source/components/datagridbeta/README.md +232 -0
  5. package/source/components/datagridbeta/__tests__/core.test.ts +341 -0
  6. package/source/components/datagridbeta/__tests__/export.test.ts +70 -0
  7. package/source/components/datagridbeta/core/columnHelper.ts +21 -0
  8. package/source/components/datagridbeta/core/columns.ts +288 -0
  9. package/source/components/datagridbeta/core/filtering.ts +237 -0
  10. package/source/components/datagridbeta/core/grid.ts +616 -0
  11. package/source/components/datagridbeta/core/index.ts +14 -0
  12. package/source/components/datagridbeta/core/rows.ts +64 -0
  13. package/source/components/datagridbeta/core/sorting.ts +111 -0
  14. package/source/components/datagridbeta/core/state.ts +82 -0
  15. package/source/components/datagridbeta/core/store.ts +52 -0
  16. package/source/components/datagridbeta/core/types.ts +266 -0
  17. package/source/components/datagridbeta/core/values.ts +98 -0
  18. package/source/components/datagridbeta/core/virtual.ts +85 -0
  19. package/source/components/datagridbeta/export/csv.ts +32 -0
  20. package/source/components/datagridbeta/export/download.ts +12 -0
  21. package/source/components/datagridbeta/export/pdf.ts +61 -0
  22. package/source/components/datagridbeta/export/table.ts +73 -0
  23. package/source/components/datagridbeta/export/xlsx.ts +145 -0
  24. package/source/components/datagridbeta/export/zip.ts +87 -0
  25. package/source/components/datagridbeta/index.ts +7 -0
  26. package/source/components/datagridbeta/react/Cell.tsx +282 -0
  27. package/source/components/datagridbeta/react/ColumnMenu.tsx +135 -0
  28. package/source/components/datagridbeta/react/DataGrid.tsx +296 -0
  29. package/source/components/datagridbeta/react/FilterForm.tsx +142 -0
  30. package/source/components/datagridbeta/react/HeaderRow.tsx +306 -0
  31. package/source/components/datagridbeta/react/Pagination.tsx +107 -0
  32. package/source/components/datagridbeta/react/Popover.tsx +79 -0
  33. package/source/components/datagridbeta/react/Row.tsx +85 -0
  34. package/source/components/datagridbeta/react/Toolbar.tsx +266 -0
  35. package/source/components/datagridbeta/react/Viewport.tsx +167 -0
  36. package/source/components/datagridbeta/react/context.ts +57 -0
  37. package/source/components/datagridbeta/react/hooks.ts +80 -0
  38. package/source/components/datagridbeta/react/icons.tsx +72 -0
  39. package/source/components/datagridbeta/react/keyboard.ts +110 -0
  40. package/source/components/datagridbeta/react/locale.ts +128 -0
  41. package/source/components/datagridbeta/styles.css +717 -0
@@ -0,0 +1,296 @@
1
+ "use client";
2
+
3
+ import {
4
+ useDeferredValue,
5
+ useImperativeHandle,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useState,
9
+ type CSSProperties,
10
+ type ReactNode,
11
+ type Ref,
12
+ } from "react";
13
+ import { computeLayout, createSelectionColumn, resolveColumns } from "../core/columns";
14
+ import { filterRows } from "../core/filtering";
15
+ import { createGridEngine, type GridApi, type GridSnapshot } from "../core/grid";
16
+ import { buildRows, createRowIdGetter, paginate } from "../core/rows";
17
+ import { sortRows } from "../core/sorting";
18
+ import { shallowEqual } from "../core/store";
19
+ import type { Density, GridOptions } from "../core/types";
20
+ import { createFormatters } from "../core/values";
21
+ import { ColumnMenu } from "./ColumnMenu";
22
+ import { GridContext, cx, type GridContextValue, type GridSlot } from "./context";
23
+ import { usePopoverState, useStoreSelector } from "./hooks";
24
+ import { defaultLocaleText, type LocaleText } from "./locale";
25
+ import { Pagination } from "./Pagination";
26
+ import { Popover } from "./Popover";
27
+ import { Toolbar, type ToolbarOptions } from "./Toolbar";
28
+ import { GridViewport } from "./Viewport";
29
+
30
+ export interface DataGridProps<T> extends GridOptions<T> {
31
+ ref?: Ref<GridApi<T>>;
32
+ /** Height of the scrolling area. Ignored with `autoHeight`. Default 520. */
33
+ height?: number | string;
34
+ /** Grow to fit all rows instead of scrolling (use with pagination or small data). */
35
+ autoHeight?: boolean;
36
+ /** Fixed row height in px. Defaults to the density's height. */
37
+ rowHeight?: number;
38
+ headerHeight?: number;
39
+ loading?: boolean;
40
+ toolbar?: boolean | ToolbarOptions;
41
+ pageSizeOptions?: number[];
42
+ emptyState?: ReactNode;
43
+ getRowClassName?(row: T, rowIndex: number): string | undefined;
44
+ classNames?: Partial<Record<GridSlot, string>>;
45
+ className?: string;
46
+ style?: CSSProperties;
47
+ /** BCP 47 locale for number and date formatting. */
48
+ locale?: string;
49
+ localeText?: Partial<LocaleText>;
50
+ "aria-label"?: string;
51
+ }
52
+
53
+ const ROW_HEIGHTS: Record<Density, number> = { compact: 32, standard: 40, comfortable: 52 };
54
+ const DEFAULT_PAGE_SIZES = [25, 50, 100, 250];
55
+ const EMPTY_CLASSNAMES: Partial<Record<GridSlot, string>> = {};
56
+
57
+ const selectModelState = ({ state }: GridSnapshot) => ({
58
+ sorting: state.sorting,
59
+ filters: state.filters,
60
+ globalFilter: state.globalFilter,
61
+ pagination: state.pagination,
62
+ columnOrder: state.columnOrder,
63
+ columnVisibility: state.columnVisibility,
64
+ columnSizing: state.columnSizing,
65
+ columnPinning: state.columnPinning,
66
+ density: state.density,
67
+ });
68
+
69
+ export function DataGrid<T>(props: DataGridProps<T>) {
70
+ const {
71
+ ref,
72
+ data,
73
+ columns: columnDefs,
74
+ getRowId,
75
+ mode = "client",
76
+ rowCount,
77
+ state: controlled,
78
+ enablePagination = true,
79
+ enableSorting = true,
80
+ enableFiltering = true,
81
+ enableColumnResizing = true,
82
+ enableColumnReordering = true,
83
+ enableColumnPinning = true,
84
+ enableColumnHiding = true,
85
+ enableRowSelection = false,
86
+ selectionMode = "multiple",
87
+ height = 520,
88
+ autoHeight = false,
89
+ loading = false,
90
+ toolbar = true,
91
+ pageSizeOptions = DEFAULT_PAGE_SIZES,
92
+ emptyState,
93
+ getRowClassName,
94
+ classNames = EMPTY_CLASSNAMES,
95
+ className,
96
+ style,
97
+ locale,
98
+ localeText: localeOverrides,
99
+ } = props;
100
+
101
+ const [engine] = useState(() => createGridEngine<T>(props));
102
+ useImperativeHandle(ref, () => engine.api, [engine]);
103
+
104
+ // Selection and UI state are deliberately not selected here: changing them
105
+ // re-renders only the rows and cells that subscribe to them.
106
+ const internal = useStoreSelector(engine.store, selectModelState, shallowEqual);
107
+ const sorting = controlled?.sorting ?? internal.sorting;
108
+ const filters = controlled?.filters ?? internal.filters;
109
+ const globalFilter = controlled?.globalFilter ?? internal.globalFilter;
110
+ const pagination = controlled?.pagination ?? internal.pagination;
111
+ const columnOrder = controlled?.columnOrder ?? internal.columnOrder;
112
+ const columnVisibility = controlled?.columnVisibility ?? internal.columnVisibility;
113
+ const columnSizing = controlled?.columnSizing ?? internal.columnSizing;
114
+ const columnPinning = controlled?.columnPinning ?? internal.columnPinning;
115
+ const density = controlled?.density ?? internal.density;
116
+
117
+ const localeText = useMemo(() => ({ ...defaultLocaleText, ...localeOverrides }), [localeOverrides]);
118
+ const formatters = useMemo(
119
+ () => createFormatters(locale, { yes: localeText.yes, no: localeText.no }),
120
+ [locale, localeText.yes, localeText.no],
121
+ );
122
+ const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);
123
+
124
+ // Row pipeline: each stage recomputes only when its own inputs change.
125
+ const server = mode === "server";
126
+ const rowIdGetter = useMemo(() => createRowIdGetter(getRowId), [getRowId]);
127
+ const columns = useMemo(() => resolveColumns(columnDefs), [columnDefs]);
128
+ const coreRows = useMemo(() => buildRows(data, rowIdGetter), [data, rowIdGetter]);
129
+
130
+ const liveQuery = useMemo(
131
+ () => ({ sorting, filters, globalFilter, pagination }),
132
+ [sorting, filters, globalFilter, pagination],
133
+ );
134
+ // Filtering and sorting large data render at low priority, so typing stays responsive.
135
+ const query = useDeferredValue(liveQuery);
136
+ const stale = query !== liveQuery;
137
+
138
+ const filteredRows = useMemo(
139
+ () => (server ? coreRows : filterRows(coreRows, columns, query.filters, query.globalFilter, formatters)),
140
+ [server, coreRows, columns, query.filters, query.globalFilter, formatters],
141
+ );
142
+ const sortedRows = useMemo(
143
+ () => (server ? filteredRows : sortRows(filteredRows, columns, query.sorting)),
144
+ [server, filteredRows, columns, query.sorting],
145
+ );
146
+ const page = useMemo(
147
+ () => paginate(sortedRows, query.pagination, { enabled: enablePagination, server, rowCount }),
148
+ [sortedRows, query.pagination, enablePagination, server, rowCount],
149
+ );
150
+
151
+ const hasSelection = enableRowSelection !== false;
152
+ const leadingColumns = useMemo(() => (hasSelection ? [createSelectionColumn<T>()] : []), [hasSelection]);
153
+ const layout = useMemo(
154
+ () => computeLayout(columns, { columnOrder, columnVisibility, columnSizing, columnPinning }, leadingColumns),
155
+ [columns, columnOrder, columnVisibility, columnSizing, columnPinning, leadingColumns],
156
+ );
157
+
158
+ const rowHeight = props.rowHeight ?? ROW_HEIGHTS[density];
159
+ const headerHeight = props.headerHeight ?? Math.max(40, rowHeight);
160
+
161
+ // Hand the committed props and model to the engine for event handlers.
162
+ useLayoutEffect(() => {
163
+ engine.sync(props, { columns, coreRows, sortedRows, page, layout, rowHeight, headerHeight, formatters });
164
+ });
165
+
166
+ const menu = usePopoverState<string>();
167
+ const openMenu = menu.open;
168
+
169
+ const isRowSelectable = useMemo(
170
+ () =>
171
+ typeof enableRowSelection === "function" ? enableRowSelection : () => enableRowSelection === true,
172
+ [enableRowSelection],
173
+ );
174
+
175
+ const context = useMemo<GridContextValue<T>>(
176
+ () => ({
177
+ engine,
178
+ locale: localeText,
179
+ classNames,
180
+ formatters,
181
+ numberFormat,
182
+ controlledSelection: controlled?.rowSelection,
183
+ isRowSelectable,
184
+ openColumnMenu: (columnId, anchor) => openMenu(anchor, columnId),
185
+ features: {
186
+ sorting: enableSorting,
187
+ filtering: enableFiltering,
188
+ resizing: enableColumnResizing,
189
+ reordering: enableColumnReordering,
190
+ pinning: enableColumnPinning,
191
+ hiding: enableColumnHiding,
192
+ selection: hasSelection,
193
+ selectionMode,
194
+ },
195
+ }),
196
+ [
197
+ engine,
198
+ localeText,
199
+ classNames,
200
+ formatters,
201
+ numberFormat,
202
+ controlled?.rowSelection,
203
+ isRowSelectable,
204
+ openMenu,
205
+ enableSorting,
206
+ enableFiltering,
207
+ enableColumnResizing,
208
+ enableColumnReordering,
209
+ enableColumnPinning,
210
+ enableColumnHiding,
211
+ hasSelection,
212
+ selectionMode,
213
+ ],
214
+ );
215
+
216
+ const toolbarOptions: ToolbarOptions | null =
217
+ toolbar === false
218
+ ? null
219
+ : {
220
+ search: true,
221
+ filterChips: true,
222
+ columns: true,
223
+ density: true,
224
+ export: true,
225
+ ...(toolbar === true ? {} : toolbar),
226
+ };
227
+
228
+ const menuAnchor = menu.state?.anchor ?? null;
229
+ const menuItem = menu.state ? layout.columns.find((i) => i.column.id === menu.state?.payload) : undefined;
230
+ const closeMenu = () => menu.close(menuAnchor);
231
+
232
+ return (
233
+ <GridContext value={context as unknown as GridContextValue<unknown>}>
234
+ <div
235
+ className={cx("dg-root", classNames.root, className)}
236
+ style={
237
+ {
238
+ ...style,
239
+ "--dg-row-height": `${rowHeight}px`,
240
+ "--dg-header-height": `${headerHeight}px`,
241
+ } as CSSProperties
242
+ }
243
+ data-density={density}
244
+ data-stale={stale || undefined}
245
+ >
246
+ {toolbarOptions && (
247
+ <Toolbar
248
+ options={toolbarOptions}
249
+ globalFilter={globalFilter}
250
+ filters={filters}
251
+ density={density}
252
+ columns={columns}
253
+ columnVisibility={columnVisibility}
254
+ />
255
+ )}
256
+
257
+ <GridViewport
258
+ layout={layout}
259
+ page={page}
260
+ sortedRows={sortedRows}
261
+ sorting={sorting}
262
+ filters={filters}
263
+ hasQuery={filters.length > 0 || globalFilter !== ""}
264
+ rowHeight={rowHeight}
265
+ headerHeight={headerHeight}
266
+ height={autoHeight ? undefined : height}
267
+ loading={loading}
268
+ emptyState={emptyState}
269
+ ariaLabel={props["aria-label"] ?? localeText.gridLabel}
270
+ getRowClassName={getRowClassName}
271
+ />
272
+
273
+ {enablePagination && (
274
+ <Pagination page={page} pageSize={pagination.pageSize} pageSizeOptions={pageSizeOptions} />
275
+ )}
276
+
277
+ {menuAnchor && menuItem && (
278
+ <Popover
279
+ anchor={menuAnchor}
280
+ onClose={closeMenu}
281
+ label={localeText.columnMenu(menuItem.column.header)}
282
+ className="dg-column-menu-popover"
283
+ >
284
+ <ColumnMenu
285
+ item={menuItem}
286
+ layout={layout}
287
+ sort={sorting.find((s) => s.columnId === menuItem.column.id)}
288
+ filter={filters.find((f) => f.columnId === menuItem.column.id)}
289
+ onClose={closeMenu}
290
+ />
291
+ </Popover>
292
+ )}
293
+ </div>
294
+ </GridContext>
295
+ );
296
+ }
@@ -0,0 +1,142 @@
1
+ "use client";
2
+
3
+ import { useState, type FormEvent } from "react";
4
+ import { getFilterOperators } from "../core/filtering";
5
+ import type { ColumnFilter, FilterOperator, FilterValue, ResolvedColumn } from "../core/types";
6
+ import { useGridContext } from "./context";
7
+
8
+ interface FilterFormProps<T> {
9
+ column: ResolvedColumn<T>;
10
+ filter: ColumnFilter | undefined;
11
+ onDone(): void;
12
+ }
13
+
14
+ const toInput = (value: FilterValue | undefined) =>
15
+ value === null || value === undefined || typeof value === "object" ? "" : String(value);
16
+
17
+ export function FilterForm<T>({ column, filter, onDone }: FilterFormProps<T>) {
18
+ const { engine, locale } = useGridContext<T>();
19
+ const operators = getFilterOperators(column);
20
+ const { options } = column.def;
21
+
22
+ const [operator, setOperator] = useState<FilterOperator>(
23
+ filter && operators.includes(filter.operator) ? filter.operator : operators[0],
24
+ );
25
+ const [value, setValue] = useState(() => toInput(filter?.value));
26
+ const [value2, setValue2] = useState(() => toInput(filter?.value2));
27
+ const [chosen, setChosen] = useState<ReadonlyArray<string | number>>(() =>
28
+ filter && Array.isArray(filter.value) ? (filter.value as ReadonlyArray<string | number>) : [],
29
+ );
30
+
31
+ const needsValue = operator !== "isEmpty" && operator !== "isNotEmpty";
32
+ const inputType = column.type === "number" ? "number" : column.type === "date" ? "date" : "text";
33
+ const parse = (raw: string): FilterValue =>
34
+ raw === "" ? null : column.type === "number" ? Number(raw) : raw;
35
+
36
+ const submit = (event: FormEvent<HTMLFormElement>) => {
37
+ event.preventDefault();
38
+ let next: Omit<ColumnFilter, "columnId"> | null;
39
+ if (!needsValue) next = { operator };
40
+ else if (operator === "in") next = chosen.length > 0 ? { operator, value: chosen } : null;
41
+ else if (column.type === "boolean") next = { operator, value: value !== "false" };
42
+ else if (operator === "between") {
43
+ next = value || value2 ? { operator, value: parse(value), value2: parse(value2) } : null;
44
+ } else next = value === "" ? null : { operator, value: parse(value) };
45
+
46
+ engine.api.setFilter(column.id, next);
47
+ onDone();
48
+ };
49
+
50
+ const clear = () => {
51
+ engine.api.setFilter(column.id, null);
52
+ onDone();
53
+ };
54
+
55
+ return (
56
+ <form className="dg-filter-form" onSubmit={submit}>
57
+ <div className="dg-menu-label">{locale.filter}</div>
58
+
59
+ {operators.length > 1 && (
60
+ <select
61
+ className="dg-select"
62
+ aria-label={locale.filterOperator}
63
+ value={operator}
64
+ onChange={(e) => setOperator(e.target.value as FilterOperator)}
65
+ >
66
+ {operators.map((op) => (
67
+ <option key={op} value={op}>
68
+ {locale.operators[op]}
69
+ </option>
70
+ ))}
71
+ </select>
72
+ )}
73
+
74
+ {needsValue && operator === "in" && options && (
75
+ <div className="dg-check-list" role="group" aria-label={locale.filterValue}>
76
+ {options.map((option) => {
77
+ const checked = chosen.includes(option.value);
78
+ return (
79
+ <label key={String(option.value)}>
80
+ <input
81
+ type="checkbox"
82
+ className="dg-checkbox"
83
+ checked={checked}
84
+ onChange={() =>
85
+ setChosen(checked ? chosen.filter((v) => v !== option.value) : [...chosen, option.value])
86
+ }
87
+ />
88
+ {option.label}
89
+ </label>
90
+ );
91
+ })}
92
+ </div>
93
+ )}
94
+
95
+ {needsValue && column.type === "boolean" && (
96
+ <select
97
+ className="dg-select"
98
+ aria-label={locale.filterValue}
99
+ value={value === "false" ? "false" : "true"}
100
+ onChange={(e) => setValue(e.target.value)}
101
+ >
102
+ <option value="true">{locale.yes}</option>
103
+ <option value="false">{locale.no}</option>
104
+ </select>
105
+ )}
106
+
107
+ {needsValue && operator !== "in" && column.type !== "boolean" && (
108
+ <>
109
+ <input
110
+ className="dg-input"
111
+ type={inputType}
112
+ value={value}
113
+ aria-label={operator === "between" ? locale.filterFrom : locale.filterValue}
114
+ placeholder={operator === "between" ? locale.filterFrom : locale.filterValue}
115
+ onChange={(e) => setValue(e.target.value)}
116
+ />
117
+ {operator === "between" && (
118
+ <input
119
+ className="dg-input"
120
+ type={inputType}
121
+ value={value2}
122
+ aria-label={locale.filterTo}
123
+ placeholder={locale.filterTo}
124
+ onChange={(e) => setValue2(e.target.value)}
125
+ />
126
+ )}
127
+ </>
128
+ )}
129
+
130
+ <div className="dg-form-actions">
131
+ {filter && (
132
+ <button type="button" className="dg-button" onClick={clear}>
133
+ {locale.clearFilter}
134
+ </button>
135
+ )}
136
+ <button type="submit" className="dg-button" data-variant="primary">
137
+ {locale.applyFilter}
138
+ </button>
139
+ </div>
140
+ </form>
141
+ );
142
+ }