@dxos/react-ui-table 0.3.8-next.f4e0086 → 0.3.9-main.03b62b6
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/dist/lib/browser/index.mjs +605 -532
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/Cell/Cell.d.ts +12 -0
- package/dist/types/src/components/Cell/Cell.d.ts.map +1 -0
- package/dist/types/src/components/{ComboboxCell.d.ts → Cell/CellCombobox.d.ts} +4 -4
- package/dist/types/src/components/Cell/CellCombobox.d.ts.map +1 -0
- package/dist/types/src/components/Cell/index.d.ts +2 -0
- package/dist/types/src/components/Cell/index.d.ts.map +1 -0
- package/dist/types/src/components/ColumnMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +3 -0
- package/dist/types/src/components/Table/Table.d.ts.map +1 -0
- package/dist/types/src/components/{Table.stories.d.ts → Table/Table.stories.d.ts} +4 -7
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableBody.d.ts +11 -0
- package/dist/types/src/components/Table/TableBody.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableContext.d.ts +14 -0
- package/dist/types/src/components/Table/TableContext.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableFooter.d.ts +7 -0
- package/dist/types/src/components/Table/TableFooter.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableHead.d.ts +8 -0
- package/dist/types/src/components/Table/TableHead.d.ts.map +1 -0
- package/dist/types/src/components/Table/index.d.ts +3 -0
- package/dist/types/src/components/Table/index.d.ts.map +1 -0
- package/dist/types/src/components/Table/props.d.ts +40 -0
- package/dist/types/src/components/Table/props.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/helpers.d.ts +21 -15
- package/dist/types/src/helpers.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/schema.d.ts +0 -1
- package/dist/types/src/schema.d.ts.map +1 -1
- package/dist/types/src/theme.d.ts +30 -33
- package/dist/types/src/theme.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +11 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +13 -10
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +12 -10
- package/src/components/Cell/Cell.tsx +43 -0
- package/src/components/{ComboboxCell.tsx → Cell/CellCombobox.tsx} +6 -6
- package/src/components/Cell/index.ts +5 -0
- package/src/components/ColumnMenu.tsx +16 -16
- package/src/components/{Table.stories.tsx → Table/Table.stories.tsx} +51 -51
- package/src/components/Table/Table.tsx +184 -0
- package/src/components/Table/TableBody.tsx +70 -0
- package/src/components/Table/TableContext.tsx +28 -0
- package/src/components/Table/TableFooter.tsx +42 -0
- package/src/components/Table/TableHead.tsx +79 -0
- package/src/components/Table/index.ts +6 -0
- package/src/components/Table/props.ts +58 -0
- package/src/components/index.ts +1 -1
- package/src/helpers.tsx +289 -219
- package/src/index.ts +1 -0
- package/src/schema.ts +0 -8
- package/src/theme.ts +102 -63
- package/src/translations.ts +11 -0
- package/src/types.ts +15 -10
- package/dist/types/src/components/ComboboxCell.d.ts.map +0 -1
- package/dist/types/src/components/Table.d.ts +0 -29
- package/dist/types/src/components/Table.d.ts.map +0 -1
- package/dist/types/src/components/Table.stories.d.ts.map +0 -1
- package/src/components/Table.tsx +0 -551
package/src/helpers.tsx
CHANGED
|
@@ -4,34 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
import { useFocusFinders } from '@fluentui/react-tabster';
|
|
6
6
|
import { Check, ClipboardText, type Icon, X } from '@phosphor-icons/react';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createColumnHelper,
|
|
9
|
+
type ColumnDef,
|
|
10
|
+
type ColumnMeta,
|
|
11
|
+
type RowData,
|
|
12
|
+
type ColumnHelper,
|
|
13
|
+
type CellContext,
|
|
14
|
+
} from '@tanstack/react-table';
|
|
8
15
|
import format from 'date-fns/format';
|
|
9
16
|
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
|
10
|
-
import
|
|
11
|
-
import React, { useRef, useState } from 'react';
|
|
17
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
12
18
|
|
|
13
19
|
import { type PublicKey } from '@dxos/keys';
|
|
14
|
-
import { Input, Tooltip } from '@dxos/react-ui';
|
|
20
|
+
import { type ClassNameValue, Input, Tooltip } from '@dxos/react-ui';
|
|
15
21
|
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
// TODO(burdon): Factor out hack to find next focusable element (extend useFocusFinders)?
|
|
21
|
-
const findNextFocusable = (
|
|
22
|
-
findFirstFocusable: (container: HTMLElement) => HTMLElement | null | undefined,
|
|
23
|
-
container: HTMLElement,
|
|
24
|
-
): HTMLElement | undefined => {
|
|
25
|
-
const next = findFirstFocusable(container as any);
|
|
26
|
-
if (next) {
|
|
27
|
-
if (next?.tagName === 'INPUT') {
|
|
28
|
-
return next;
|
|
29
|
-
}
|
|
30
|
-
if (container.nextElementSibling) {
|
|
31
|
-
return findNextFocusable(findFirstFocusable, container.nextSibling as any);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
};
|
|
22
|
+
|
|
23
|
+
import { CellCombobox } from './components';
|
|
24
|
+
import { useTableContext } from './components/Table/TableContext';
|
|
25
|
+
import { textPadding } from './theme';
|
|
35
26
|
|
|
36
27
|
export type ValueUpdater<TData extends RowData, TValue> = (row: TData, id: string, value: TValue) => void;
|
|
37
28
|
|
|
@@ -40,28 +31,30 @@ export const createColumnBuilder = <TData extends RowData>() => ({
|
|
|
40
31
|
builder: new ColumnBuilder<TData>(),
|
|
41
32
|
});
|
|
42
33
|
|
|
34
|
+
// TODO(thure): This interface is redundant, e.g. `classNames` == `meta.cell.classNames`
|
|
43
35
|
/**
|
|
44
36
|
* NOTE: Can use `meta` for custom properties.
|
|
45
37
|
*/
|
|
46
38
|
export type BaseColumnOptions<TData, TValue> = Partial<ColumnDef<TData, TValue>> & {
|
|
47
39
|
meta?: ColumnMeta<TData, TValue>;
|
|
48
40
|
label?: string;
|
|
49
|
-
|
|
41
|
+
classNames?: ClassNameValue;
|
|
50
42
|
onUpdate?: ValueUpdater<TData, TValue | undefined>;
|
|
51
43
|
};
|
|
52
44
|
|
|
53
45
|
// TODO(burdon): Better abstraction?
|
|
54
|
-
export type
|
|
46
|
+
export type SearchListQueryModel<TData extends RowData> = {
|
|
55
47
|
getId(object: TData): string;
|
|
56
48
|
getText(object: TData): string;
|
|
57
49
|
query(text?: string): Promise<TData[]>;
|
|
58
50
|
};
|
|
59
51
|
|
|
60
|
-
export type
|
|
61
|
-
model:
|
|
52
|
+
export type ComboboxColumnOptions<TData extends RowData> = BaseColumnOptions<TData, any> & {
|
|
53
|
+
model: SearchListQueryModel<TData>;
|
|
62
54
|
};
|
|
63
55
|
|
|
64
56
|
export type StringColumnOptions<TData extends RowData> = BaseColumnOptions<TData, string> & {};
|
|
57
|
+
export type SelectRowColumnOptions<TData extends RowData> = BaseColumnOptions<TData, string> & {};
|
|
65
58
|
|
|
66
59
|
export type NumberColumnOptions<TData extends RowData> = BaseColumnOptions<TData, number> & {
|
|
67
60
|
digits?: number;
|
|
@@ -76,7 +69,7 @@ export type DateColumnOptions<TData extends RowData> = BaseColumnOptions<TData,
|
|
|
76
69
|
relative?: boolean;
|
|
77
70
|
};
|
|
78
71
|
|
|
79
|
-
export type
|
|
72
|
+
export type SwitchColumnOptions<TData extends RowData> = BaseColumnOptions<TData, boolean> & {};
|
|
80
73
|
|
|
81
74
|
export type IconColumnOptions<TData extends RowData> = BaseColumnOptions<TData, boolean> & {
|
|
82
75
|
on?: {
|
|
@@ -89,11 +82,158 @@ export type IconColumnOptions<TData extends RowData> = BaseColumnOptions<TData,
|
|
|
89
82
|
};
|
|
90
83
|
};
|
|
91
84
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
const ComboboxBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, TData>) => {
|
|
86
|
+
const { model, onUpdate } = cellContext.column.columnDef.meta as ColumnMeta<TData, TData>;
|
|
87
|
+
return model ? (
|
|
88
|
+
<CellCombobox<TData>
|
|
89
|
+
model={model}
|
|
90
|
+
value={cellContext.getValue()}
|
|
91
|
+
onValueChange={(value) => onUpdate?.(cellContext.row.original, cellContext.column.id, value)}
|
|
92
|
+
/>
|
|
93
|
+
) : null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const StringBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, string>) => {
|
|
97
|
+
const { onUpdate } = cellContext.column.columnDef.meta as ColumnMeta<TData, string>;
|
|
98
|
+
// https://tanstack.com/table/v8/docs/examples/react/editable-data
|
|
99
|
+
const initialValue = cellContext.getValue();
|
|
100
|
+
const [value, setValue] = useState(initialValue);
|
|
101
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
102
|
+
|
|
103
|
+
const handleSave = () => {
|
|
104
|
+
if (value === initialValue) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
onUpdate?.(cellContext.row.original, cellContext.column.id, value);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleCancel = () => {
|
|
111
|
+
setValue(initialValue);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Check if first input column of last row.
|
|
115
|
+
const rows = cellContext.table.getRowModel().flatRows;
|
|
116
|
+
const columns = cellContext.table.getVisibleFlatColumns();
|
|
117
|
+
const placeholder = cellContext.row.index === rows.length - 1 && columns[0].id === cellContext.column.id;
|
|
118
|
+
|
|
119
|
+
// TODO(burdon): Don't render inputs unless mouse over (Show ellipsis when div)?
|
|
120
|
+
return (
|
|
121
|
+
<Input.Root>
|
|
122
|
+
<Input.TextInput
|
|
123
|
+
ref={inputRef}
|
|
124
|
+
variant='subdued'
|
|
125
|
+
placeholder={placeholder ? 'Add row...' : undefined}
|
|
126
|
+
classNames={['is-full', textPadding]}
|
|
127
|
+
value={value ?? ''}
|
|
128
|
+
onBlur={() => handleSave()}
|
|
129
|
+
onChange={(event) => setValue(event.target.value)}
|
|
130
|
+
onKeyDown={(event) => (event.key === 'Enter' && handleSave()) || (event.key === 'Escape' && handleCancel())}
|
|
131
|
+
/>
|
|
132
|
+
</Input.Root>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const parseNumber = (text?: string) => (text?.trim().length ? Number(text) : NaN);
|
|
137
|
+
|
|
138
|
+
const displayNumber = (value?: number, digits?: number) =>
|
|
139
|
+
value || value === 0
|
|
140
|
+
? value.toLocaleString(undefined, {
|
|
141
|
+
minimumFractionDigits: digits ?? 0,
|
|
142
|
+
maximumFractionDigits: digits ?? 2,
|
|
143
|
+
})
|
|
144
|
+
: '';
|
|
145
|
+
|
|
146
|
+
const NumberBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, number>) => {
|
|
147
|
+
const { onUpdate, digits } = cellContext.column.columnDef.meta as ColumnMeta<TData, string>;
|
|
148
|
+
const value = cellContext.getValue();
|
|
149
|
+
const [text, setText] = useState<string>(displayNumber(value, digits));
|
|
150
|
+
const { findPrevFocusable } = useFocusFinders();
|
|
151
|
+
const ref = useRef<HTMLInputElement | null>(null);
|
|
152
|
+
|
|
153
|
+
// Update display value if `digits` changes and input is not in focus.
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
(!document.activeElement || document.activeElement !== ref.current) && setText(displayNumber(value, digits));
|
|
156
|
+
}, [digits, value]);
|
|
157
|
+
|
|
158
|
+
const handleEdit = () => {
|
|
159
|
+
if (onUpdate) {
|
|
160
|
+
setText(value !== undefined ? String(value) : '');
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// TODO(burdon): Property is encoded as float (e.g., 6.1 => 6.099)
|
|
165
|
+
const handleSave = () => {
|
|
166
|
+
const nextValue = parseNumber(text);
|
|
167
|
+
onUpdate?.(cellContext.row.original, cellContext.column.id, isNaN(nextValue) ? undefined : nextValue);
|
|
168
|
+
setText(displayNumber(nextValue, digits));
|
|
169
|
+
// Shift focus only if input is focused on save (e.g. on `Enter`).
|
|
170
|
+
ref.current && document.activeElement === ref.current && findPrevFocusable(ref.current)?.focus();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const _handleCancel = () => {
|
|
174
|
+
setText(displayNumber(value, digits));
|
|
175
|
+
ref.current && findPrevFocusable(ref.current)?.focus();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<Input.Root>
|
|
180
|
+
<Input.TextInput
|
|
181
|
+
ref={ref}
|
|
182
|
+
variant='subdued'
|
|
183
|
+
value={text}
|
|
184
|
+
classNames={['is-full text-end font-mono', textPadding]}
|
|
185
|
+
onBlur={handleSave}
|
|
186
|
+
onFocus={handleEdit}
|
|
187
|
+
onChange={({ target: { value } }) => setText(value)}
|
|
188
|
+
onKeyDown={(event) => {
|
|
189
|
+
// TODO(thure): `Escape` is reserved for blurring the input and moving focus to the cell
|
|
190
|
+
return /* (event.key === 'Escape' && handleCancel()) || */ event.key === 'Enter' && handleSave();
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
</Input.Root>
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const RowSelectorBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, unknown>) => {
|
|
198
|
+
const { cell } = cellContext.column.columnDef.meta as ColumnMeta<TData, unknown>;
|
|
199
|
+
const { row } = cellContext;
|
|
200
|
+
|
|
201
|
+
const checked = row.getCanSelect()
|
|
202
|
+
? row.getIsSelected()
|
|
203
|
+
: row.getCanSelectSubRows() && (row.getIsSomeSelected() ? 'indeterminate' : row.getIsAllSubRowsSelected());
|
|
204
|
+
return (
|
|
205
|
+
<Input.Root>
|
|
206
|
+
<Input.Checkbox
|
|
207
|
+
size={4}
|
|
208
|
+
classNames={['mli-auto', cell?.classNames]}
|
|
209
|
+
checked={checked}
|
|
210
|
+
onCheckedChange={(event) => {
|
|
211
|
+
if (row.getCanSelect()) {
|
|
212
|
+
row.getToggleSelectedHandler()(event);
|
|
213
|
+
}
|
|
214
|
+
}}
|
|
215
|
+
disabled={!(row.getCanSelect() || row.getCanSelectSubRows())}
|
|
216
|
+
/>
|
|
217
|
+
</Input.Root>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const SwitchBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, boolean>) => {
|
|
222
|
+
const { onUpdate } = cellContext.column.columnDef.meta as ColumnMeta<TData, boolean>;
|
|
223
|
+
const value = cellContext.getValue();
|
|
224
|
+
return (
|
|
225
|
+
<Input.Root>
|
|
226
|
+
<Input.Switch
|
|
227
|
+
classNames='block mli-auto'
|
|
228
|
+
onClick={(event) => event.stopPropagation()}
|
|
229
|
+
disabled={!onUpdate}
|
|
230
|
+
checked={!!value}
|
|
231
|
+
onCheckedChange={(value) => {
|
|
232
|
+
onUpdate?.(cellContext.row.original, cellContext.column.id, !!value);
|
|
233
|
+
}}
|
|
234
|
+
/>
|
|
235
|
+
</Input.Root>
|
|
236
|
+
);
|
|
97
237
|
};
|
|
98
238
|
|
|
99
239
|
/**
|
|
@@ -101,176 +241,97 @@ const defaults = <TData extends RowData, TValue>(
|
|
|
101
241
|
*/
|
|
102
242
|
export class ColumnBuilder<TData extends RowData> {
|
|
103
243
|
/**
|
|
104
|
-
*
|
|
244
|
+
* Combobox value
|
|
105
245
|
*/
|
|
106
|
-
|
|
107
|
-
|
|
246
|
+
combobox({
|
|
247
|
+
label,
|
|
248
|
+
classNames,
|
|
249
|
+
model,
|
|
250
|
+
onUpdate,
|
|
251
|
+
...props
|
|
252
|
+
}: ComboboxColumnOptions<TData>): Partial<ColumnDef<TData, any>> {
|
|
253
|
+
return {
|
|
108
254
|
minSize: 100,
|
|
109
255
|
header: (column) => {
|
|
110
|
-
return
|
|
256
|
+
return label ?? column.header.id;
|
|
111
257
|
},
|
|
112
258
|
cell:
|
|
113
259
|
model && onUpdate
|
|
114
|
-
?
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
onValueChange={(value) => onUpdate?.(cell.row.original, cell.column.id, value)}
|
|
119
|
-
/>
|
|
120
|
-
)
|
|
121
|
-
: (cell) => {
|
|
122
|
-
const value = cell.getValue();
|
|
123
|
-
return <div className={mx('truncate', className)}>{value ? model.getText(value) : ''}</div>;
|
|
260
|
+
? ComboboxBuilderCell
|
|
261
|
+
: (cellContext) => {
|
|
262
|
+
const cellValue = cellContext.getValue();
|
|
263
|
+
return <div className={mx('truncate', classNames)}>{cellValue ? model?.getText(cellValue) : ''}</div>;
|
|
124
264
|
},
|
|
125
|
-
|
|
265
|
+
...props,
|
|
266
|
+
meta: {
|
|
267
|
+
...props.meta,
|
|
268
|
+
model,
|
|
269
|
+
onUpdate,
|
|
270
|
+
cell: { classNames, ...props.meta?.cell },
|
|
271
|
+
},
|
|
272
|
+
};
|
|
126
273
|
}
|
|
127
274
|
|
|
128
275
|
/**
|
|
129
276
|
* String formats.
|
|
130
277
|
*/
|
|
131
|
-
string({ label,
|
|
132
|
-
|
|
278
|
+
string({ label, classNames, onUpdate, ...props }: StringColumnOptions<TData> = {}): Partial<
|
|
279
|
+
ColumnDef<TData, string>
|
|
280
|
+
> {
|
|
281
|
+
return {
|
|
133
282
|
minSize: 100,
|
|
134
283
|
// TODO(burdon): Default.
|
|
135
284
|
header: (column) => {
|
|
136
|
-
return
|
|
285
|
+
return label ?? column.header.id;
|
|
137
286
|
},
|
|
138
287
|
cell: onUpdate
|
|
139
|
-
?
|
|
140
|
-
// https://tanstack.com/table/v8/docs/examples/react/editable-data
|
|
141
|
-
const initialValue = cell.getValue();
|
|
142
|
-
const [value, setValue] = useState(initialValue);
|
|
143
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
144
|
-
const { findFirstFocusable } = useFocusFinders();
|
|
145
|
-
|
|
146
|
-
const handleSave = (focusNext = false) => {
|
|
147
|
-
if (value === initialValue) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
onUpdate?.(cell.row.original, cell.column.id, value);
|
|
152
|
-
|
|
153
|
-
// TODO(burdon): More generally support keyboard navigation.
|
|
154
|
-
if (focusNext) {
|
|
155
|
-
const rowElement = inputRef.current?.parentElement?.parentElement;
|
|
156
|
-
// TODO(burdon): Hack to wait for next row to render.
|
|
157
|
-
setTimeout(() => {
|
|
158
|
-
const next = findNextFocusable(findFirstFocusable, rowElement as HTMLElement);
|
|
159
|
-
next?.focus();
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const handleCancel = () => {
|
|
165
|
-
setValue(initialValue);
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
// Check if first input column of last row.
|
|
169
|
-
const rows = cell.table.getRowModel().flatRows;
|
|
170
|
-
const columns = cell.table.getVisibleFlatColumns();
|
|
171
|
-
const placeholder = cell.row.index === rows.length - 1 && columns[0].id === cell.column.id;
|
|
172
|
-
|
|
173
|
-
// TODO(burdon): Don't render inputs unless mouse over (Show ellipsis when div)?
|
|
174
|
-
return (
|
|
175
|
-
<Input.Root>
|
|
176
|
-
<Input.TextInput
|
|
177
|
-
ref={inputRef}
|
|
178
|
-
variant='subdued'
|
|
179
|
-
placeholder={placeholder ? 'Add row...' : undefined}
|
|
180
|
-
classNames={'px-2'}
|
|
181
|
-
value={(value as string) ?? ''}
|
|
182
|
-
onBlur={() => handleSave(false)}
|
|
183
|
-
onChange={(event) => setValue(event.target.value)}
|
|
184
|
-
onKeyDown={(event) =>
|
|
185
|
-
(event.key === 'Enter' && handleSave(true)) || (event.key === 'Escape' && handleCancel())
|
|
186
|
-
}
|
|
187
|
-
/>
|
|
188
|
-
</Input.Root>
|
|
189
|
-
);
|
|
190
|
-
}
|
|
288
|
+
? StringBuilderCell
|
|
191
289
|
: (cell) => {
|
|
192
290
|
const value = cell.getValue();
|
|
193
|
-
return <div className={mx('truncate',
|
|
291
|
+
return <div className={mx('truncate', textPadding, classNames)}>{value}</div>;
|
|
194
292
|
},
|
|
195
|
-
|
|
293
|
+
...props,
|
|
294
|
+
meta: {
|
|
295
|
+
...props.meta,
|
|
296
|
+
onUpdate,
|
|
297
|
+
cell: { ...props.meta?.cell, classNames },
|
|
298
|
+
},
|
|
299
|
+
};
|
|
196
300
|
}
|
|
197
301
|
|
|
198
302
|
/**
|
|
199
303
|
* Number formats.
|
|
200
304
|
*/
|
|
201
|
-
number({ label, minSize,
|
|
305
|
+
number({ label, minSize, classNames, digits, onUpdate, ...props }: NumberColumnOptions<TData> = {}): Partial<
|
|
202
306
|
ColumnDef<TData, number>
|
|
203
307
|
> {
|
|
204
|
-
return
|
|
308
|
+
return {
|
|
205
309
|
size: 100,
|
|
206
310
|
minSize: 100,
|
|
207
|
-
header: (cell) =>
|
|
208
|
-
cell:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
// TODO(burdon): Property is encoded as float (e.g., 6.1 => 6.099)
|
|
219
|
-
const handleSave = () => {
|
|
220
|
-
const value = text?.trim().length ? Number(text) : NaN;
|
|
221
|
-
onUpdate?.(cell.row.original, cell.column.id, isNaN(value) ? undefined : value);
|
|
222
|
-
setText(undefined);
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const handleCancel = () => {
|
|
226
|
-
setText(undefined);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
if (text !== undefined) {
|
|
230
|
-
return (
|
|
231
|
-
<div className={mx('grow text-right font-mono', className)}>
|
|
232
|
-
<Input.Root>
|
|
233
|
-
<Input.TextInput
|
|
234
|
-
autoFocus
|
|
235
|
-
value={text}
|
|
236
|
-
classNames={'px-2'}
|
|
237
|
-
onBlur={handleSave}
|
|
238
|
-
onChange={(event) => setText(event.target.value)}
|
|
239
|
-
onKeyDown={(event) =>
|
|
240
|
-
(event.key === 'Escape' && handleCancel()) || (event.key === 'Enter' && handleSave())
|
|
241
|
-
}
|
|
242
|
-
/>
|
|
243
|
-
</Input.Root>
|
|
244
|
-
</div>
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return (
|
|
249
|
-
<div
|
|
250
|
-
className={mx('grow w-full text-right font-mono empty:after:content-[" "]', className)}
|
|
251
|
-
onClick={handleEdit}
|
|
252
|
-
>
|
|
253
|
-
{value?.toLocaleString(undefined, {
|
|
254
|
-
minimumFractionDigits: digits ?? 0,
|
|
255
|
-
maximumFractionDigits: digits ?? 2,
|
|
256
|
-
})}
|
|
257
|
-
</div>
|
|
258
|
-
);
|
|
311
|
+
header: (cell) => label ?? cell.header.id,
|
|
312
|
+
cell: NumberBuilderCell,
|
|
313
|
+
...props,
|
|
314
|
+
meta: {
|
|
315
|
+
...props.meta,
|
|
316
|
+
onUpdate,
|
|
317
|
+
digits,
|
|
318
|
+
header: { classNames: 'text-end', ...props.meta?.header },
|
|
319
|
+
cell: { ...props.meta?.cell, classNames },
|
|
259
320
|
},
|
|
260
|
-
}
|
|
321
|
+
};
|
|
261
322
|
}
|
|
262
323
|
|
|
263
324
|
/**
|
|
264
325
|
* Date formats.
|
|
265
326
|
*/
|
|
266
327
|
// TODO(burdon): Date picker (pluggable renderers?)
|
|
267
|
-
date({ label, format: formatSpec, relative,
|
|
328
|
+
date({ label, format: formatSpec, relative, classNames, ...props }: DateColumnOptions<TData> = {}): Partial<
|
|
268
329
|
ColumnDef<TData, Date>
|
|
269
330
|
> {
|
|
270
|
-
return
|
|
331
|
+
return {
|
|
271
332
|
size: 220, // TODO(burdon): Depends on format.
|
|
272
333
|
minSize: 100,
|
|
273
|
-
header: (cell) =>
|
|
334
|
+
header: (cell) => label ?? cell.header.id,
|
|
274
335
|
cell: (cell) => {
|
|
275
336
|
const value = cell.getValue();
|
|
276
337
|
|
|
@@ -283,23 +344,25 @@ export class ColumnBuilder<TData extends RowData> {
|
|
|
283
344
|
: value.toISOString()
|
|
284
345
|
: undefined;
|
|
285
346
|
|
|
286
|
-
return <div className={mx(
|
|
347
|
+
return <div className={mx(textPadding, classNames)}>{str}</div>;
|
|
287
348
|
} catch (err) {
|
|
288
349
|
console.log(value);
|
|
289
350
|
return null;
|
|
290
351
|
}
|
|
291
352
|
},
|
|
292
|
-
|
|
353
|
+
...props,
|
|
354
|
+
meta: { ...props.meta, cell: { ...props.meta?.cell, classNames } },
|
|
355
|
+
};
|
|
293
356
|
}
|
|
294
357
|
|
|
295
358
|
/**
|
|
296
359
|
* PublicKey with tooltip.
|
|
297
360
|
*/
|
|
298
|
-
key({ label, tooltip, ...props }: KeyColumnOptions<TData> = {}): Partial<ColumnDef<TData, PublicKey>> {
|
|
299
|
-
return
|
|
300
|
-
size:
|
|
301
|
-
minSize:
|
|
302
|
-
header: (cell) =>
|
|
361
|
+
key({ label, tooltip, classNames, ...props }: KeyColumnOptions<TData> = {}): Partial<ColumnDef<TData, PublicKey>> {
|
|
362
|
+
return {
|
|
363
|
+
size: 94,
|
|
364
|
+
minSize: 94,
|
|
365
|
+
header: (cell) => label ?? cell.header.id,
|
|
303
366
|
cell: (cell) => {
|
|
304
367
|
const value = cell.getValue();
|
|
305
368
|
if (!value) {
|
|
@@ -307,18 +370,16 @@ export class ColumnBuilder<TData extends RowData> {
|
|
|
307
370
|
}
|
|
308
371
|
|
|
309
372
|
// TODO(burdon): Factor out styles.
|
|
310
|
-
const element = (
|
|
311
|
-
<div className='font-mono font-thin text-green-500 dark:text-green-300'>{value.truncate()}</div>
|
|
312
|
-
);
|
|
373
|
+
const element = value.truncate();
|
|
313
374
|
if (!tooltip) {
|
|
314
375
|
return element;
|
|
315
376
|
}
|
|
316
377
|
|
|
317
378
|
return (
|
|
318
|
-
<
|
|
319
|
-
<Tooltip.
|
|
320
|
-
<Tooltip.
|
|
321
|
-
|
|
379
|
+
<Tooltip.Provider>
|
|
380
|
+
<Tooltip.Root>
|
|
381
|
+
<Tooltip.Trigger>{element}</Tooltip.Trigger>
|
|
382
|
+
<Tooltip.Portal>
|
|
322
383
|
<Tooltip.Content side='right'>
|
|
323
384
|
<Tooltip.Arrow />
|
|
324
385
|
<ClipboardText
|
|
@@ -328,44 +389,60 @@ export class ColumnBuilder<TData extends RowData> {
|
|
|
328
389
|
}}
|
|
329
390
|
/>
|
|
330
391
|
</Tooltip.Content>
|
|
331
|
-
</Tooltip.
|
|
332
|
-
</Tooltip.
|
|
333
|
-
</
|
|
392
|
+
</Tooltip.Portal>
|
|
393
|
+
</Tooltip.Root>
|
|
394
|
+
</Tooltip.Provider>
|
|
334
395
|
);
|
|
335
396
|
},
|
|
336
|
-
|
|
397
|
+
...props,
|
|
398
|
+
meta: { ...props.meta, cell: { ...props.meta?.cell, classNames: ['font-mono', textPadding, classNames] } },
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Row selector
|
|
404
|
+
*/
|
|
405
|
+
selectRow({ classNames, onUpdate, id = 'selectRow' }: SelectRowColumnOptions<TData> = {}): Parameters<
|
|
406
|
+
ColumnHelper<TData>['display']
|
|
407
|
+
>[0] {
|
|
408
|
+
return {
|
|
409
|
+
id,
|
|
410
|
+
size: 32,
|
|
411
|
+
minSize: 32,
|
|
412
|
+
maxSize: 32,
|
|
413
|
+
meta: { onUpdate, cell: { classNames } },
|
|
414
|
+
header: ({ table }) => {
|
|
415
|
+
const { rowsSelectable } = useTableContext('HELPER_SELECT_ROW_HEADER_CELL');
|
|
416
|
+
const checked = table.getIsSomeRowsSelected() ? 'indeterminate' : table.getIsAllRowsSelected();
|
|
417
|
+
return rowsSelectable === 'multi' ? (
|
|
418
|
+
<Input.Root>
|
|
419
|
+
<Input.Checkbox
|
|
420
|
+
size={4}
|
|
421
|
+
classNames={['mli-auto', classNames]}
|
|
422
|
+
checked={checked}
|
|
423
|
+
onCheckedChange={() => table.toggleAllRowsSelected()}
|
|
424
|
+
/>
|
|
425
|
+
</Input.Root>
|
|
426
|
+
) : null;
|
|
427
|
+
},
|
|
428
|
+
cell: RowSelectorBuilderCell,
|
|
429
|
+
};
|
|
337
430
|
}
|
|
338
431
|
|
|
339
432
|
/**
|
|
340
|
-
*
|
|
433
|
+
* Switch
|
|
341
434
|
*/
|
|
342
|
-
|
|
435
|
+
switch({ label, classNames, onUpdate, ...props }: SwitchColumnOptions<TData> = {}): Partial<
|
|
343
436
|
ColumnDef<TData, boolean>
|
|
344
437
|
> {
|
|
345
|
-
return
|
|
346
|
-
size:
|
|
347
|
-
minSize:
|
|
348
|
-
header: (column) =>
|
|
349
|
-
cell:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
<div className='flex grow justify-center'>
|
|
354
|
-
<Input.Root>
|
|
355
|
-
<Input.Checkbox
|
|
356
|
-
onClick={(event) => event.stopPropagation()}
|
|
357
|
-
classNames={className}
|
|
358
|
-
disabled={!onUpdate}
|
|
359
|
-
checked={!!value}
|
|
360
|
-
onCheckedChange={(value) => {
|
|
361
|
-
onUpdate?.(cell.row.original, cell.column.id, !!value);
|
|
362
|
-
}}
|
|
363
|
-
/>
|
|
364
|
-
</Input.Root>
|
|
365
|
-
</div>
|
|
366
|
-
);
|
|
367
|
-
},
|
|
368
|
-
});
|
|
438
|
+
return {
|
|
439
|
+
size: 50,
|
|
440
|
+
minSize: 50,
|
|
441
|
+
header: (column) => label ?? column.header.id,
|
|
442
|
+
cell: SwitchBuilderCell,
|
|
443
|
+
...props,
|
|
444
|
+
meta: { ...props.meta, onUpdate, cell: { ...props.meta?.cell, classNames: [textPadding, classNames] } },
|
|
445
|
+
};
|
|
369
446
|
}
|
|
370
447
|
|
|
371
448
|
/**
|
|
@@ -374,27 +451,20 @@ export class ColumnBuilder<TData extends RowData> {
|
|
|
374
451
|
icon({ label, size, on, off, ...props }: IconColumnOptions<TData> = {}): Partial<ColumnDef<TData, boolean>> {
|
|
375
452
|
const IconOn = on?.Icon ?? Check;
|
|
376
453
|
const IconOff = off?.Icon ?? X;
|
|
377
|
-
return
|
|
454
|
+
return {
|
|
378
455
|
size: size ?? 32,
|
|
379
456
|
header: (column) => <div className={'justify-center'}>{label ?? column.header.id}</div>,
|
|
380
457
|
cell: (cell) => {
|
|
381
458
|
const value = cell.getValue();
|
|
382
459
|
if (value) {
|
|
383
|
-
return (
|
|
384
|
-
<div className='flex grow justify-center'>
|
|
385
|
-
<IconOn className={mx(getSize(6), on?.className ?? 'text-green-700')} />
|
|
386
|
-
</div>
|
|
387
|
-
);
|
|
460
|
+
return <IconOn className={mx('block mli-auto', getSize(6), on?.className ?? 'text-green-700')} />;
|
|
388
461
|
} else if (value === false) {
|
|
389
|
-
return (
|
|
390
|
-
<div className='flex grow justify-center'>
|
|
391
|
-
<IconOff className={mx(getSize(6), off?.className ?? 'text-red-700')} />
|
|
392
|
-
</div>
|
|
393
|
-
);
|
|
462
|
+
return <IconOff className={mx('block mli-auto', getSize(6), off?.className ?? 'text-red-700')} />;
|
|
394
463
|
} else {
|
|
395
464
|
return null;
|
|
396
465
|
}
|
|
397
466
|
},
|
|
398
|
-
|
|
467
|
+
...props,
|
|
468
|
+
};
|
|
399
469
|
}
|
|
400
470
|
}
|
package/src/index.ts
CHANGED
package/src/schema.ts
CHANGED
|
@@ -10,14 +10,6 @@ export type TableDef = {
|
|
|
10
10
|
|
|
11
11
|
export type ColumnType = 'number' | 'boolean' | 'date' | 'string' | 'ref';
|
|
12
12
|
|
|
13
|
-
export const columnTypes = new Map<ColumnType, string>([
|
|
14
|
-
['string', 'Text'],
|
|
15
|
-
['boolean', 'Checkbox'],
|
|
16
|
-
['number', 'Number'],
|
|
17
|
-
['date', 'Date'],
|
|
18
|
-
['ref', 'Reference'],
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
13
|
export type ColumnProps = {
|
|
22
14
|
id: string;
|
|
23
15
|
prop: string;
|