gbs-add-block 1.2.6 → 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 (72) hide show
  1. package/README.md +3 -3
  2. package/index.cjs +10 -9
  3. package/package.json +1 -1
  4. package/source/components/button/index.tsx +2 -2
  5. package/source/components/card/index.tsx +1 -1
  6. package/source/components/checkbox/index.tsx +1 -1
  7. package/source/components/datagrid/context/GridContext.tsx +38 -17
  8. package/source/components/datagrid/createTemplate.tsx +2 -2
  9. package/source/components/datagrid/hooks/usePagination.ts +0 -8
  10. package/source/components/datagrid/hooks/useSearch.ts +3 -3
  11. package/source/components/datagrid/index.tsx +2 -1
  12. package/source/components/datagrid/layout/GridBody.tsx +7 -4
  13. package/source/components/datagrid/layout/GridPagination.tsx +3 -2
  14. package/source/components/datagrid/layout/GridToolbar.tsx +2 -1
  15. package/source/components/datagrid/type.ts +18 -2
  16. package/source/components/datagridbeta/README.md +232 -0
  17. package/source/components/datagridbeta/__tests__/core.test.ts +341 -0
  18. package/source/components/datagridbeta/__tests__/export.test.ts +70 -0
  19. package/source/components/datagridbeta/core/columnHelper.ts +21 -0
  20. package/source/components/datagridbeta/core/columns.ts +288 -0
  21. package/source/components/datagridbeta/core/filtering.ts +237 -0
  22. package/source/components/datagridbeta/core/grid.ts +616 -0
  23. package/source/components/datagridbeta/core/index.ts +14 -0
  24. package/source/components/datagridbeta/core/rows.ts +64 -0
  25. package/source/components/datagridbeta/core/sorting.ts +111 -0
  26. package/source/components/datagridbeta/core/state.ts +82 -0
  27. package/source/components/datagridbeta/core/store.ts +52 -0
  28. package/source/components/datagridbeta/core/types.ts +266 -0
  29. package/source/components/datagridbeta/core/values.ts +98 -0
  30. package/source/components/datagridbeta/core/virtual.ts +85 -0
  31. package/source/components/datagridbeta/export/csv.ts +32 -0
  32. package/source/components/datagridbeta/export/download.ts +12 -0
  33. package/source/components/datagridbeta/export/pdf.ts +61 -0
  34. package/source/components/datagridbeta/export/table.ts +73 -0
  35. package/source/components/datagridbeta/export/xlsx.ts +145 -0
  36. package/source/components/datagridbeta/export/zip.ts +87 -0
  37. package/source/components/datagridbeta/index.ts +7 -0
  38. package/source/components/datagridbeta/react/Cell.tsx +282 -0
  39. package/source/components/datagridbeta/react/ColumnMenu.tsx +135 -0
  40. package/source/components/datagridbeta/react/DataGrid.tsx +296 -0
  41. package/source/components/datagridbeta/react/FilterForm.tsx +142 -0
  42. package/source/components/datagridbeta/react/HeaderRow.tsx +306 -0
  43. package/source/components/datagridbeta/react/Pagination.tsx +107 -0
  44. package/source/components/datagridbeta/react/Popover.tsx +79 -0
  45. package/source/components/datagridbeta/react/Row.tsx +85 -0
  46. package/source/components/datagridbeta/react/Toolbar.tsx +266 -0
  47. package/source/components/datagridbeta/react/Viewport.tsx +167 -0
  48. package/source/components/datagridbeta/react/context.ts +57 -0
  49. package/source/components/datagridbeta/react/hooks.ts +80 -0
  50. package/source/components/datagridbeta/react/icons.tsx +72 -0
  51. package/source/components/datagridbeta/react/keyboard.ts +110 -0
  52. package/source/components/datagridbeta/react/locale.ts +128 -0
  53. package/source/components/datagridbeta/styles.css +717 -0
  54. package/source/components/datepicker/index.tsx +7 -7
  55. package/source/components/input/index.tsx +1 -1
  56. package/source/components/multiselect/PortalDropDown.tsx +1 -1
  57. package/source/components/multiselect/index.tsx +6 -6
  58. package/source/components/navbar/AuthSection.tsx +5 -5
  59. package/source/components/navbar/NavItemComponent.tsx +2 -2
  60. package/source/components/select/PortalDropDown.tsx +1 -1
  61. package/source/components/select/index.tsx +5 -5
  62. package/source/components/spinner/index.tsx +2 -2
  63. package/source/components/tabs/index.tsx +6 -6
  64. package/source/components/textarea/index.tsx +9 -9
  65. package/source/components/toast/ToastComponent.tsx +26 -25
  66. package/source/components/toast/ToastIcon.tsx +8 -8
  67. package/source/components/uploader/ProgressAnimation.tsx +2 -2
  68. package/source/components/uploader/UploadedFilePreview.tsx +1 -1
  69. package/source/components/uploader/index.tsx +1 -1
  70. package/source/components/uploader/uploaderIcon.tsx +1 -1
  71. package/source/globalStyle.ts +29 -16
  72. package/source/theme.ts +7 -7
@@ -0,0 +1,282 @@
1
+ "use client";
2
+
3
+ import {
4
+ memo,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ type FocusEvent,
9
+ type KeyboardEvent,
10
+ type ReactNode,
11
+ } from "react";
12
+ import type { MoveDirection } from "../core/grid";
13
+ import type { ColumnLayoutItem, GridRow, ResolvedColumn } from "../core/types";
14
+ import { formatCellValue, toDateInputValue, toTime } from "../core/values";
15
+ import { cx, NO_PENDING, useGridContext } from "./context";
16
+ import { useFocusWhenActive } from "./hooks";
17
+ import { CheckIcon } from "./icons";
18
+
19
+ interface CellProps<T> {
20
+ row: GridRow<T>;
21
+ rowIndex: number;
22
+ item: ColumnLayoutItem<T>;
23
+ active: boolean;
24
+ editing: boolean;
25
+ /** Value of an in-flight async edit, or NO_PENDING. */
26
+ pendingValue: unknown;
27
+ error: string | undefined;
28
+ selected: boolean;
29
+ selectable: boolean;
30
+ }
31
+
32
+ const noop = () => {};
33
+
34
+ function CellView<T>({ row, rowIndex, item, active, editing, pendingValue, error, selected, selectable }: CellProps<T>) {
35
+ const { engine, locale, classNames, formatters } = useGridContext<T>();
36
+ const ref = useRef<HTMLDivElement>(null);
37
+ useFocusWhenActive(ref, active, editing);
38
+
39
+ const { column } = item;
40
+ const hasPending = pendingValue !== NO_PENDING;
41
+ const value = hasPending ? pendingValue : column.getValue(row.original);
42
+ const editable = !column.internal && engine.isCellEditable(row.original, column);
43
+
44
+ let content: ReactNode;
45
+ if (column.internal) {
46
+ content = (
47
+ <input
48
+ type="checkbox"
49
+ className="dg-checkbox"
50
+ tabIndex={-1}
51
+ aria-label={locale.selectRow}
52
+ checked={selected}
53
+ disabled={!selectable}
54
+ onChange={noop}
55
+ onClick={(event) => {
56
+ event.stopPropagation();
57
+ engine.api.toggleRowSelected(row.id, { range: event.shiftKey });
58
+ }}
59
+ />
60
+ );
61
+ } else if (editing) {
62
+ content = <CellEditor row={row} column={column} initialValue={value} />;
63
+ } else if (column.def.cell) {
64
+ content = column.def.cell({ row: row.original, rowId: row.id, rowIndex, value, column });
65
+ } else if (column.type === "boolean" && !column.def.format) {
66
+ content = (
67
+ <span className="dg-bool">
68
+ {value ? <CheckIcon /> : null}
69
+ <span className="dg-sr-only">{value ? locale.yes : locale.no}</span>
70
+ </span>
71
+ );
72
+ } else {
73
+ const text = formatCellValue(column, value, row.original, formatters);
74
+ content = <span className="dg-cell-text">{text}</span>;
75
+ }
76
+
77
+ const { cellClassName } = column.def;
78
+ const extraClass =
79
+ typeof cellClassName === "function"
80
+ ? cellClassName({ row: row.original, rowId: row.id, rowIndex, value, column })
81
+ : cellClassName;
82
+
83
+ return (
84
+ <div
85
+ ref={ref}
86
+ role="gridcell"
87
+ aria-colindex={item.visibleIndex + 1}
88
+ aria-invalid={error ? true : undefined}
89
+ tabIndex={active ? 0 : -1}
90
+ title={error}
91
+ className={cx("dg-cell", classNames.cell, extraClass)}
92
+ style={item.style}
93
+ data-col-index={item.visibleIndex}
94
+ data-pinned={item.pinned || undefined}
95
+ data-pin-edge={item.pinEdge || undefined}
96
+ data-align={column.align}
97
+ data-active={active || undefined}
98
+ data-editable={editable || undefined}
99
+ data-editing={editing || undefined}
100
+ data-pending={hasPending || undefined}
101
+ data-invalid={error ? "" : undefined}
102
+ onClick={() => engine.setActiveCell(rowIndex, item.visibleIndex, { focus: false, scroll: false })}
103
+ onDoubleClick={editable ? () => engine.api.startEditing(row.id, column.id) : undefined}
104
+ >
105
+ {content}
106
+ </div>
107
+ );
108
+ }
109
+
110
+ export const Cell = memo(CellView) as typeof CellView;
111
+
112
+ interface CellEditorProps<T> {
113
+ row: GridRow<T>;
114
+ column: ResolvedColumn<T>;
115
+ initialValue: unknown;
116
+ }
117
+
118
+ function fromDateInput(raw: string, original: unknown): unknown {
119
+ if (raw === "") return null;
120
+ if (original instanceof Date) return new Date(toTime(raw)!);
121
+ if (typeof original === "number") return toTime(raw);
122
+ return raw;
123
+ }
124
+
125
+ function CellEditor<T>({ row, column, initialValue }: CellEditorProps<T>) {
126
+ const { engine } = useGridContext<T>();
127
+ const [draft, setDraft] = useState(initialValue);
128
+ const [error, setError] = useState<string | null>(null);
129
+ const containerRef = useRef<HTMLDivElement>(null);
130
+
131
+ useLayoutEffect(() => {
132
+ const target = containerRef.current?.querySelector<HTMLElement>("input, select, textarea, button, [tabindex]");
133
+ target?.focus({ preventScroll: true });
134
+ if (target instanceof HTMLInputElement && (target.type === "text" || target.type === "number")) {
135
+ target.select();
136
+ }
137
+ }, []);
138
+
139
+ // The engine ignores commits for a cell that is no longer being edited, so a
140
+ // blur that follows Enter or Escape is harmless.
141
+ const commit = (value: unknown = draft, move?: MoveDirection) => {
142
+ const message = engine.commitEdit(row.id, column.id, value, move);
143
+ if (message) setError(message);
144
+ };
145
+
146
+ const cancel = () => engine.api.cancelEditing();
147
+
148
+ const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
149
+ if (event.key === "Escape") {
150
+ event.preventDefault();
151
+ cancel();
152
+ } else if (event.key === "Enter" && !(event.target instanceof HTMLTextAreaElement)) {
153
+ event.preventDefault();
154
+ commit(draft, event.shiftKey ? "up" : "down");
155
+ } else if (event.key === "Tab") {
156
+ event.preventDefault();
157
+ commit(draft, event.shiftKey ? "left" : "right");
158
+ }
159
+ };
160
+
161
+ const onBlur = (event: FocusEvent<HTMLDivElement>) => {
162
+ if (!event.currentTarget.contains(event.relatedTarget)) commit();
163
+ };
164
+
165
+ const { editor, options } = column.def;
166
+ const invalid = error ? true : undefined;
167
+ let control: ReactNode;
168
+
169
+ if (typeof editor === "function") {
170
+ control = editor({
171
+ value: draft,
172
+ row: row.original,
173
+ column,
174
+ error,
175
+ onChange: setDraft,
176
+ commit: (value) => commit(value === undefined ? draft : value),
177
+ cancel,
178
+ });
179
+ } else {
180
+ const kind =
181
+ editor ??
182
+ (options
183
+ ? "select"
184
+ : column.type === "number"
185
+ ? "number"
186
+ : column.type === "date"
187
+ ? "date"
188
+ : column.type === "boolean"
189
+ ? "checkbox"
190
+ : "text");
191
+
192
+ switch (kind) {
193
+ case "select":
194
+ control = (
195
+ <select
196
+ className="dg-editor-input"
197
+ aria-label={column.header}
198
+ aria-invalid={invalid}
199
+ value={String(draft ?? "")}
200
+ onChange={(event) => {
201
+ const option = options?.find((o) => String(o.value) === event.target.value);
202
+ setDraft(option ? option.value : event.target.value);
203
+ }}
204
+ >
205
+ {options?.map((option) => (
206
+ <option key={String(option.value)} value={String(option.value)}>
207
+ {option.label}
208
+ </option>
209
+ ))}
210
+ </select>
211
+ );
212
+ break;
213
+ case "checkbox":
214
+ control = (
215
+ <input
216
+ type="checkbox"
217
+ className="dg-checkbox"
218
+ aria-label={column.header}
219
+ aria-invalid={invalid}
220
+ checked={Boolean(draft)}
221
+ onChange={(event) => setDraft(event.target.checked)}
222
+ />
223
+ );
224
+ break;
225
+ case "date":
226
+ control = (
227
+ <input
228
+ type="date"
229
+ className="dg-editor-input"
230
+ aria-label={column.header}
231
+ aria-invalid={invalid}
232
+ value={toDateInputValue(draft)}
233
+ onChange={(event) => setDraft(fromDateInput(event.target.value, initialValue))}
234
+ />
235
+ );
236
+ break;
237
+ case "number":
238
+ control = (
239
+ <input
240
+ type="number"
241
+ className="dg-editor-input"
242
+ aria-label={column.header}
243
+ aria-invalid={invalid}
244
+ value={typeof draft === "number" && !Number.isNaN(draft) ? String(draft) : ""}
245
+ onChange={(event) =>
246
+ setDraft(event.target.value === "" ? null : event.target.valueAsNumber)
247
+ }
248
+ />
249
+ );
250
+ break;
251
+ default:
252
+ control = (
253
+ <input
254
+ type="text"
255
+ className="dg-editor-input"
256
+ aria-label={column.header}
257
+ aria-invalid={invalid}
258
+ value={draft === null || draft === undefined ? "" : String(draft)}
259
+ onChange={(event) => setDraft(event.target.value)}
260
+ />
261
+ );
262
+ }
263
+ }
264
+
265
+ return (
266
+ <div
267
+ ref={containerRef}
268
+ className="dg-editor"
269
+ onKeyDown={onKeyDown}
270
+ onBlur={onBlur}
271
+ onClick={(event) => event.stopPropagation()}
272
+ onDoubleClick={(event) => event.stopPropagation()}
273
+ >
274
+ {control}
275
+ {error && (
276
+ <span className="dg-editor-error" role="alert">
277
+ {error}
278
+ </span>
279
+ )}
280
+ </div>
281
+ );
282
+ }
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import type { ColumnFilter, ColumnLayout, ColumnLayoutItem, SortItem } from "../core/types";
4
+ import { useGridContext } from "./context";
5
+ import { FilterForm } from "./FilterForm";
6
+ import { ArrowDownIcon, ArrowUpIcon, EyeOffIcon, MoveIcon, PinIcon, XIcon } from "./icons";
7
+
8
+ interface ColumnMenuProps<T> {
9
+ item: ColumnLayoutItem<T>;
10
+ layout: ColumnLayout<T>;
11
+ sort: SortItem | undefined;
12
+ filter: ColumnFilter | undefined;
13
+ onClose(): void;
14
+ }
15
+
16
+ export function ColumnMenu<T>({ item, layout, sort, filter, onClose }: ColumnMenuProps<T>) {
17
+ const { engine, locale, features } = useGridContext<T>();
18
+ const { api } = engine;
19
+ const { column } = item;
20
+
21
+ const run = (action: () => void) => () => {
22
+ action();
23
+ onClose();
24
+ };
25
+
26
+ // Keyboard alternative to drag-and-drop: move within the column's own pin section.
27
+ const section = (item.pinned === "left" ? layout.left : item.pinned === "right" ? layout.right : layout.center)
28
+ .filter((i) => !i.column.internal);
29
+ const position = section.findIndex((i) => i.column.id === column.id);
30
+ const previous = section[position - 1];
31
+ const next = section[position + 1];
32
+
33
+ const canSort = features.sorting && column.sortable;
34
+ const canPin = features.pinning && column.pinnable;
35
+ const canMove = features.reordering && column.reorderable && (previous || next);
36
+ const canHide = features.hiding && column.hideable;
37
+ const canFilter = features.filtering && column.filterable;
38
+
39
+ return (
40
+ <div className="dg-column-menu">
41
+ {canSort && (
42
+ <div className="dg-menu-section">
43
+ <button
44
+ type="button"
45
+ className="dg-menu-item"
46
+ aria-pressed={sort?.desc === false}
47
+ onClick={run(() => api.setSorting([{ columnId: column.id, desc: false }]))}
48
+ >
49
+ <ArrowUpIcon /> {locale.sortAscending}
50
+ </button>
51
+ <button
52
+ type="button"
53
+ className="dg-menu-item"
54
+ aria-pressed={sort?.desc === true}
55
+ onClick={run(() => api.setSorting([{ columnId: column.id, desc: true }]))}
56
+ >
57
+ <ArrowDownIcon /> {locale.sortDescending}
58
+ </button>
59
+ {sort && (
60
+ <button
61
+ type="button"
62
+ className="dg-menu-item"
63
+ onClick={run(() =>
64
+ api.setSorting(api.getState().sorting.filter((s) => s.columnId !== column.id)),
65
+ )}
66
+ >
67
+ <XIcon /> {locale.clearSort}
68
+ </button>
69
+ )}
70
+ </div>
71
+ )}
72
+
73
+ {canPin && (
74
+ <div className="dg-menu-section">
75
+ {item.pinned !== "left" && (
76
+ <button type="button" className="dg-menu-item" onClick={run(() => api.pinColumn(column.id, "left"))}>
77
+ <PinIcon /> {locale.pinLeft}
78
+ </button>
79
+ )}
80
+ {item.pinned !== "right" && (
81
+ <button type="button" className="dg-menu-item" onClick={run(() => api.pinColumn(column.id, "right"))}>
82
+ <PinIcon /> {locale.pinRight}
83
+ </button>
84
+ )}
85
+ {item.pinned && (
86
+ <button type="button" className="dg-menu-item" onClick={run(() => api.pinColumn(column.id, false))}>
87
+ <XIcon /> {locale.unpin}
88
+ </button>
89
+ )}
90
+ </div>
91
+ )}
92
+
93
+ {canMove && (
94
+ <div className="dg-menu-section">
95
+ {previous && (
96
+ <button
97
+ type="button"
98
+ className="dg-menu-item"
99
+ onClick={run(() => api.moveColumn(column.id, previous.column.id, "before"))}
100
+ >
101
+ <MoveIcon /> {locale.moveLeft}
102
+ </button>
103
+ )}
104
+ {next && (
105
+ <button
106
+ type="button"
107
+ className="dg-menu-item"
108
+ onClick={run(() => api.moveColumn(column.id, next.column.id, "after"))}
109
+ >
110
+ <MoveIcon /> {locale.moveRight}
111
+ </button>
112
+ )}
113
+ </div>
114
+ )}
115
+
116
+ {canHide && (
117
+ <div className="dg-menu-section">
118
+ <button
119
+ type="button"
120
+ className="dg-menu-item"
121
+ onClick={run(() => api.setColumnVisibility(column.id, false))}
122
+ >
123
+ <EyeOffIcon /> {locale.hideColumn}
124
+ </button>
125
+ </div>
126
+ )}
127
+
128
+ {canFilter && (
129
+ <div className="dg-menu-section">
130
+ <FilterForm column={column} filter={filter} onDone={onClose} />
131
+ </div>
132
+ )}
133
+ </div>
134
+ );
135
+ }