@cdx-ui/data-grid 0.3.12 → 0.3.13
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/coverage.svg +1 -1
- package/lib/commonjs/DataGrid.js +279 -40
- package/lib/commonjs/DataGrid.js.map +1 -1
- package/lib/commonjs/DataGridFilterPanel.js +136 -9
- package/lib/commonjs/DataGridFilterPanel.js.map +1 -1
- package/lib/commonjs/adapter/columns.js +16 -8
- package/lib/commonjs/adapter/columns.js.map +1 -1
- package/lib/commonjs/adapter/pinning.js +5 -1
- package/lib/commonjs/adapter/pinning.js.map +1 -1
- package/lib/commonjs/adapter/selectColumn.js +11 -0
- package/lib/commonjs/adapter/selectColumn.js.map +1 -1
- package/lib/commonjs/adapter/treeData.js +195 -0
- package/lib/commonjs/adapter/treeData.js.map +1 -0
- package/lib/commonjs/styles.js +48 -2
- package/lib/commonjs/styles.js.map +1 -1
- package/lib/commonjs/types.js +11 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/useDataGridTable.js +143 -12
- package/lib/commonjs/useDataGridTable.js.map +1 -1
- package/lib/module/DataGrid.js +285 -46
- package/lib/module/DataGrid.js.map +1 -1
- package/lib/module/DataGridFilterPanel.js +139 -13
- package/lib/module/DataGridFilterPanel.js.map +1 -1
- package/lib/module/adapter/columns.js +17 -9
- package/lib/module/adapter/columns.js.map +1 -1
- package/lib/module/adapter/pinning.js +5 -1
- package/lib/module/adapter/pinning.js.map +1 -1
- package/lib/module/adapter/selectColumn.js +11 -1
- package/lib/module/adapter/selectColumn.js.map +1 -1
- package/lib/module/adapter/treeData.js +187 -0
- package/lib/module/adapter/treeData.js.map +1 -0
- package/lib/module/styles.js +47 -1
- package/lib/module/styles.js.map +1 -1
- package/lib/module/types.js +10 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/useDataGridTable.js +147 -16
- package/lib/module/useDataGridTable.js.map +1 -1
- package/lib/typescript/DataGrid.d.ts.map +1 -1
- package/lib/typescript/DataGridFilterPanel.d.ts +39 -4
- package/lib/typescript/DataGridFilterPanel.d.ts.map +1 -1
- package/lib/typescript/adapter/columns.d.ts +1 -1
- package/lib/typescript/adapter/columns.d.ts.map +1 -1
- package/lib/typescript/adapter/pinning.d.ts +1 -0
- package/lib/typescript/adapter/pinning.d.ts.map +1 -1
- package/lib/typescript/adapter/selectColumn.d.ts +8 -0
- package/lib/typescript/adapter/selectColumn.d.ts.map +1 -1
- package/lib/typescript/adapter/treeData.d.ts +36 -0
- package/lib/typescript/adapter/treeData.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/styles.d.ts +12 -1
- package/lib/typescript/styles.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +68 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/useDataGridTable.d.ts +25 -3
- package/lib/typescript/useDataGridTable.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/DataGrid.tsx +399 -59
- package/src/DataGridFilterPanel.tsx +188 -17
- package/src/adapter/columns.ts +21 -7
- package/src/adapter/pinning.ts +10 -1
- package/src/adapter/selectColumn.ts +12 -1
- package/src/adapter/treeData.ts +228 -0
- package/src/index.ts +4 -0
- package/src/styles.ts +57 -1
- package/src/types.ts +76 -0
- package/src/useDataGridTable.ts +217 -15
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { useRef, type RefObject } from 'react';
|
|
2
|
-
import { View } from 'react-native';
|
|
3
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type RefObject } from 'react';
|
|
2
|
+
import { Platform, View } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
OverlayContainer,
|
|
5
|
+
useAnchoredOverlay,
|
|
6
|
+
useDismissOverlay,
|
|
7
|
+
useEscapeKey,
|
|
8
|
+
} from '@cdx-ui/primitives';
|
|
9
|
+
import { mergeRefs } from '@cdx-ui/utils';
|
|
4
10
|
import {
|
|
5
11
|
Button,
|
|
6
12
|
DateField,
|
|
@@ -19,6 +25,7 @@ import {
|
|
|
19
25
|
getFilterOperatorLabel,
|
|
20
26
|
getGridFilterOperators,
|
|
21
27
|
headerFilterValueFromText,
|
|
28
|
+
isActiveFilterItem,
|
|
22
29
|
} from './adapter/filterOperators';
|
|
23
30
|
import {
|
|
24
31
|
dataGridFilterPanelColumnVariants,
|
|
@@ -171,6 +178,108 @@ export function createDefaultFilterItem<R extends GridValidRowModel>(
|
|
|
171
178
|
return { field: col.field, operator: getDefaultFilterOperator(col.type) };
|
|
172
179
|
}
|
|
173
180
|
|
|
181
|
+
function createFilterItemForField<R extends GridValidRowModel>(
|
|
182
|
+
columns: readonly GridColDef<R>[],
|
|
183
|
+
field: string | undefined,
|
|
184
|
+
): GridFilterItem | undefined {
|
|
185
|
+
const col =
|
|
186
|
+
field === undefined ? undefined : columns.find((candidate) => candidate.field === field);
|
|
187
|
+
return col && col.filterable !== false
|
|
188
|
+
? { field: col.field, operator: getDefaultFilterOperator(col.type) }
|
|
189
|
+
: undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const FOCUSABLE_SELECTOR = 'input, textarea, button, [tabindex]:not([tabindex="-1"])';
|
|
193
|
+
|
|
194
|
+
function focusFirstControl(container: View | null): boolean {
|
|
195
|
+
const element = container as unknown as HTMLElement | null;
|
|
196
|
+
const control = element?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
|
|
197
|
+
if (!control) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
control.focus();
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function focusOpener(ref: RefObject<View | null>) {
|
|
205
|
+
const element = ref.current as unknown as HTMLElement | null;
|
|
206
|
+
if (Platform.OS === 'web' && typeof element?.focus === 'function') {
|
|
207
|
+
element.focus();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The control that opened the panel and, for a column opener, the field it targets. */
|
|
212
|
+
export interface DataGridFilterPanelOpener {
|
|
213
|
+
ref: RefObject<View | null>;
|
|
214
|
+
field?: string;
|
|
215
|
+
/** Changes on every open-for-field call, so a repeat call moves focus again. */
|
|
216
|
+
request?: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface UseDataGridFilterPanelParams<R extends GridValidRowModel> {
|
|
220
|
+
/** The panel's columns. A field that is missing or has `filterable: false` does not open it. */
|
|
221
|
+
columns: readonly GridColDef<R>[];
|
|
222
|
+
filterModel: GridFilterModel;
|
|
223
|
+
/** Receives the appended item. It filters nothing, so callers should not reset the page. */
|
|
224
|
+
onFilterModelChange: (model: GridFilterModel) => void;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface UseDataGridFilterPanelResult {
|
|
228
|
+
opener: DataGridFilterPanelOpener | null;
|
|
229
|
+
/** Opens the panel on `field`, or retargets it when already open. Header filter cells and the
|
|
230
|
+
* column menu call this; the column menu passes its trigger, since the menu item unmounts. */
|
|
231
|
+
openForField: (field: string, ref: RefObject<View | null>) => void;
|
|
232
|
+
/** The toolbar button: opens the panel on the model as it is, or closes it. */
|
|
233
|
+
toggle: (ref: RefObject<View | null>) => void;
|
|
234
|
+
close: (options: { restoreFocus: boolean }) => void;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Open state for the filter panel. Opening for a field shows the field's first item, even a
|
|
239
|
+
* value-less one, and leaves the model alone. When the field has none, it appends
|
|
240
|
+
* `{ field, operator }` and drops value-less items on other fields, as the append branch of MUI X
|
|
241
|
+
* `showFilterPanel(field)` does. MUI also rewrites the model when the field has an item.
|
|
242
|
+
*/
|
|
243
|
+
export function useDataGridFilterPanel<R extends GridValidRowModel>({
|
|
244
|
+
columns,
|
|
245
|
+
filterModel,
|
|
246
|
+
onFilterModelChange,
|
|
247
|
+
}: UseDataGridFilterPanelParams<R>): UseDataGridFilterPanelResult {
|
|
248
|
+
const [opener, setOpener] = useState<DataGridFilterPanelOpener | null>(null);
|
|
249
|
+
const requestRef = useRef(0);
|
|
250
|
+
|
|
251
|
+
const openForField = useCallback(
|
|
252
|
+
(field: string, ref: RefObject<View | null>) => {
|
|
253
|
+
const item = createFilterItemForField(columns, field);
|
|
254
|
+
if (!item) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!filterModel.some((candidate) => candidate.field === field)) {
|
|
258
|
+
onFilterModelChange([...filterModel.filter(isActiveFilterItem), item]);
|
|
259
|
+
}
|
|
260
|
+
requestRef.current += 1;
|
|
261
|
+
setOpener({ ref, field, request: requestRef.current });
|
|
262
|
+
},
|
|
263
|
+
[columns, filterModel, onFilterModelChange],
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const toggle = useCallback((ref: RefObject<View | null>) => {
|
|
267
|
+
setOpener((current) => (current?.ref === ref ? null : { ref }));
|
|
268
|
+
}, []);
|
|
269
|
+
|
|
270
|
+
const close = useCallback(
|
|
271
|
+
({ restoreFocus }: { restoreFocus: boolean }) => {
|
|
272
|
+
if (opener && restoreFocus) {
|
|
273
|
+
focusOpener(opener.ref);
|
|
274
|
+
}
|
|
275
|
+
setOpener(null);
|
|
276
|
+
},
|
|
277
|
+
[opener],
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
return { opener, openForField, toggle, close };
|
|
281
|
+
}
|
|
282
|
+
|
|
174
283
|
function resolveItemOperator<R extends GridValidRowModel>(
|
|
175
284
|
col: GridColDef<R> | undefined,
|
|
176
285
|
item: GridFilterItem,
|
|
@@ -199,6 +308,8 @@ function itemWithValueText<R extends GridValidRowModel>(
|
|
|
199
308
|
|
|
200
309
|
function FilterPanelRow<R extends GridValidRowModel>({
|
|
201
310
|
index,
|
|
311
|
+
operatorFieldRef,
|
|
312
|
+
valueFieldRef,
|
|
202
313
|
item,
|
|
203
314
|
columns,
|
|
204
315
|
logicOperator,
|
|
@@ -207,6 +318,8 @@ function FilterPanelRow<R extends GridValidRowModel>({
|
|
|
207
318
|
onRemove,
|
|
208
319
|
}: {
|
|
209
320
|
index: number;
|
|
321
|
+
operatorFieldRef?: RefObject<View | null>;
|
|
322
|
+
valueFieldRef?: RefObject<View | null>;
|
|
210
323
|
item: GridFilterItem;
|
|
211
324
|
columns: readonly GridColDef<R>[];
|
|
212
325
|
logicOperator: GridLogicOperator;
|
|
@@ -293,7 +406,10 @@ function FilterPanelRow<R extends GridValidRowModel>({
|
|
|
293
406
|
</Select.Content>
|
|
294
407
|
</Select>
|
|
295
408
|
</View>
|
|
296
|
-
<View
|
|
409
|
+
<View
|
|
410
|
+
ref={operatorFieldRef}
|
|
411
|
+
className={dataGridFilterPanelFieldVariants({ size: 'operator' })}
|
|
412
|
+
>
|
|
297
413
|
<Text className="text-xs text-content-secondary">Operator</Text>
|
|
298
414
|
<Select
|
|
299
415
|
items={operators.map((candidate) => ({
|
|
@@ -332,7 +448,7 @@ function FilterPanelRow<R extends GridValidRowModel>({
|
|
|
332
448
|
</Select.Content>
|
|
333
449
|
</Select>
|
|
334
450
|
</View>
|
|
335
|
-
<View className={dataGridFilterPanelFieldVariants({ size: 'value' })}>
|
|
451
|
+
<View ref={valueFieldRef} className={dataGridFilterPanelFieldVariants({ size: 'value' })}>
|
|
336
452
|
{filterOperatorNeedsValue(operator) ? (
|
|
337
453
|
<>
|
|
338
454
|
<Text className="text-xs text-content-secondary">Value</Text>
|
|
@@ -393,9 +509,10 @@ export interface DataGridFilterPanelProps<R extends GridValidRowModel> {
|
|
|
393
509
|
onFilterModelChange: (model: GridFilterModel) => void;
|
|
394
510
|
logicOperator: GridLogicOperator;
|
|
395
511
|
onLogicOperatorChange: (operator: GridLogicOperator) => void;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
512
|
+
/** `restoreFocus` is false for an outside press, which leaves focus where the user pressed. */
|
|
513
|
+
onClose: (options: { restoreFocus: boolean }) => void;
|
|
514
|
+
/** Presses on the opener are not outside presses. A column opener also anchors the panel. */
|
|
515
|
+
opener: DataGridFilterPanelOpener;
|
|
399
516
|
}
|
|
400
517
|
|
|
401
518
|
export function DataGridFilterPanel<R extends GridValidRowModel>({
|
|
@@ -405,14 +522,58 @@ export function DataGridFilterPanel<R extends GridValidRowModel>({
|
|
|
405
522
|
logicOperator,
|
|
406
523
|
onLogicOperatorChange,
|
|
407
524
|
onClose,
|
|
408
|
-
|
|
525
|
+
opener,
|
|
409
526
|
}: DataGridFilterPanelProps<R>) {
|
|
527
|
+
const anchored = opener.field !== undefined;
|
|
410
528
|
const panelRef = useRef<View>(null);
|
|
411
|
-
|
|
412
|
-
|
|
529
|
+
const operatorFieldRef = useRef<View>(null);
|
|
530
|
+
const valueFieldRef = useRef<View>(null);
|
|
531
|
+
const focusedRequestRef = useRef<number | undefined>(undefined);
|
|
532
|
+
const {
|
|
533
|
+
floatingRef,
|
|
534
|
+
onFloatingLayout,
|
|
535
|
+
style: anchoredStyle,
|
|
536
|
+
position,
|
|
537
|
+
} = useAnchoredOverlay(opener.ref, anchored, {
|
|
538
|
+
placement: 'bottom-start',
|
|
539
|
+
collisionBehavior: 'flip-shift',
|
|
540
|
+
width: 'content',
|
|
541
|
+
});
|
|
542
|
+
const dismiss = () => onClose({ restoreFocus: false });
|
|
543
|
+
const closeAndRestoreFocus = () => onClose({ restoreFocus: true });
|
|
544
|
+
useDismissOverlay(true, panelRef, [opener.ref], dismiss);
|
|
545
|
+
useEscapeKey(true, closeAndRestoreFocus);
|
|
413
546
|
|
|
414
547
|
const defaultItem = createDefaultFilterItem(columns);
|
|
415
|
-
const
|
|
548
|
+
const placeholderItem = createFilterItemForField(columns, opener.field) ?? defaultItem;
|
|
549
|
+
const items = filterModel.length > 0 ? filterModel : placeholderItem ? [placeholderItem] : [];
|
|
550
|
+
const targetIndex =
|
|
551
|
+
opener.field === undefined ? -1 : items.findIndex((item) => item.field === opener.field);
|
|
552
|
+
const targetItem = targetIndex >= 0 ? items[targetIndex] : undefined;
|
|
553
|
+
const targetNeedsValue =
|
|
554
|
+
targetItem !== undefined &&
|
|
555
|
+
filterOperatorNeedsValue(
|
|
556
|
+
resolveItemOperator(
|
|
557
|
+
columns.find((candidate) => candidate.field === targetItem.field),
|
|
558
|
+
targetItem,
|
|
559
|
+
),
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
// Rows are keyed by index, so an appended target row can mount a render after the open request.
|
|
563
|
+
// Retry on each commit until the target control exists, then stop for that request.
|
|
564
|
+
useEffect(() => {
|
|
565
|
+
if (
|
|
566
|
+
Platform.OS !== 'web' ||
|
|
567
|
+
opener.request === undefined ||
|
|
568
|
+
focusedRequestRef.current === opener.request ||
|
|
569
|
+
targetItem === undefined
|
|
570
|
+
) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (focusFirstControl((targetNeedsValue ? valueFieldRef : operatorFieldRef).current)) {
|
|
574
|
+
focusedRequestRef.current = opener.request;
|
|
575
|
+
}
|
|
576
|
+
});
|
|
416
577
|
|
|
417
578
|
const replaceItem = (index: number, next: GridFilterItem) => {
|
|
418
579
|
onFilterModelChange(items.map((item, itemIndex) => (itemIndex === index ? next : item)));
|
|
@@ -421,14 +582,20 @@ export function DataGridFilterPanel<R extends GridValidRowModel>({
|
|
|
421
582
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
422
583
|
onFilterModelChange(next);
|
|
423
584
|
if (next.length === 0) {
|
|
424
|
-
|
|
585
|
+
closeAndRestoreFocus();
|
|
425
586
|
}
|
|
426
587
|
};
|
|
427
588
|
|
|
428
|
-
|
|
589
|
+
const panel = (
|
|
429
590
|
<View
|
|
430
|
-
ref={panelRef}
|
|
431
|
-
className={dataGridFilterPanelVariants()}
|
|
591
|
+
ref={anchored ? mergeRefs(panelRef, floatingRef) : panelRef}
|
|
592
|
+
className={dataGridFilterPanelVariants({ anchored })}
|
|
593
|
+
style={
|
|
594
|
+
anchored
|
|
595
|
+
? [anchoredStyle, position ? { maxWidth: position.availableWidth } : undefined]
|
|
596
|
+
: undefined
|
|
597
|
+
}
|
|
598
|
+
onLayout={anchored ? onFloatingLayout : undefined}
|
|
432
599
|
accessibilityLabel="Filters"
|
|
433
600
|
testID="data-grid-filter-panel"
|
|
434
601
|
>
|
|
@@ -436,6 +603,8 @@ export function DataGridFilterPanel<R extends GridValidRowModel>({
|
|
|
436
603
|
<FilterPanelRow
|
|
437
604
|
key={index}
|
|
438
605
|
index={index}
|
|
606
|
+
operatorFieldRef={index === targetIndex ? operatorFieldRef : undefined}
|
|
607
|
+
valueFieldRef={index === targetIndex ? valueFieldRef : undefined}
|
|
439
608
|
item={item}
|
|
440
609
|
columns={columns}
|
|
441
610
|
logicOperator={logicOperator}
|
|
@@ -465,7 +634,7 @@ export function DataGridFilterPanel<R extends GridValidRowModel>({
|
|
|
465
634
|
size="small"
|
|
466
635
|
onPress={() => {
|
|
467
636
|
onFilterModelChange([]);
|
|
468
|
-
|
|
637
|
+
closeAndRestoreFocus();
|
|
469
638
|
}}
|
|
470
639
|
>
|
|
471
640
|
<Button.Icon as={Delete} />
|
|
@@ -474,4 +643,6 @@ export function DataGridFilterPanel<R extends GridValidRowModel>({
|
|
|
474
643
|
</View>
|
|
475
644
|
</View>
|
|
476
645
|
);
|
|
646
|
+
|
|
647
|
+
return anchored ? <OverlayContainer onDismiss={dismiss}>{panel}</OverlayContainer> : panel;
|
|
477
648
|
}
|
package/src/adapter/columns.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TableAlignment } from '@cdx-ui/components';
|
|
2
2
|
import type { ColumnPinningState } from '@tanstack/react-table';
|
|
3
3
|
import {
|
|
4
|
+
DETAIL_PANEL_TOGGLE_ID,
|
|
4
5
|
MUI_CHECKBOX_FIELD,
|
|
5
6
|
SELECT_COLUMN_ID,
|
|
6
7
|
type GridAlignment,
|
|
@@ -116,6 +117,7 @@ export function normalizePinnedColumns(
|
|
|
116
117
|
pinnedColumns: GridPinnedColumnFields | undefined,
|
|
117
118
|
knownFields: ReadonlySet<string>,
|
|
118
119
|
checkboxSelection: boolean,
|
|
120
|
+
detailPanel = false,
|
|
119
121
|
): ColumnPinningState {
|
|
120
122
|
if (pinnedColumns == null) {
|
|
121
123
|
return { start: [], end: [] };
|
|
@@ -134,8 +136,7 @@ export function normalizePinnedColumns(
|
|
|
134
136
|
if (seen.has(field)) {
|
|
135
137
|
continue;
|
|
136
138
|
}
|
|
137
|
-
|
|
138
|
-
if (isSelect ? !checkboxSelection : !knownFields.has(field)) {
|
|
139
|
+
if (!isKnownPinField(field)) {
|
|
139
140
|
warnUnknownPinnedField(raw);
|
|
140
141
|
continue;
|
|
141
142
|
}
|
|
@@ -144,6 +145,16 @@ export function normalizePinnedColumns(
|
|
|
144
145
|
}
|
|
145
146
|
};
|
|
146
147
|
|
|
148
|
+
const isKnownPinField = (field: string) => {
|
|
149
|
+
if (field === SELECT_COLUMN_ID) {
|
|
150
|
+
return checkboxSelection;
|
|
151
|
+
}
|
|
152
|
+
if (field === DETAIL_PANEL_TOGGLE_ID) {
|
|
153
|
+
return detailPanel;
|
|
154
|
+
}
|
|
155
|
+
return knownFields.has(field);
|
|
156
|
+
};
|
|
157
|
+
|
|
147
158
|
const startSeen = new Set<string>();
|
|
148
159
|
const start: string[] = [];
|
|
149
160
|
appendUnique(start, startSeen, pinnedColumns.start);
|
|
@@ -166,8 +177,7 @@ export function normalizePinnedColumns(
|
|
|
166
177
|
if (endSeen.has(field)) {
|
|
167
178
|
continue;
|
|
168
179
|
}
|
|
169
|
-
|
|
170
|
-
if (isSelect ? !checkboxSelection : !knownFields.has(field)) {
|
|
180
|
+
if (!isKnownPinField(field)) {
|
|
171
181
|
warnUnknownPinnedField(raw);
|
|
172
182
|
continue;
|
|
173
183
|
}
|
|
@@ -179,10 +189,14 @@ export function normalizePinnedColumns(
|
|
|
179
189
|
// eslint-disable-next-line @typescript-eslint/no-deprecated -- alias merge
|
|
180
190
|
appendEnd(pinnedColumns.right);
|
|
181
191
|
|
|
182
|
-
if (
|
|
183
|
-
const
|
|
192
|
+
if (start.length > 0) {
|
|
193
|
+
const leading = [
|
|
194
|
+
...(checkboxSelection ? [SELECT_COLUMN_ID] : []),
|
|
195
|
+
...(detailPanel ? [DETAIL_PANEL_TOGGLE_ID] : []),
|
|
196
|
+
];
|
|
197
|
+
const rest = start.filter((field) => !leading.includes(field));
|
|
184
198
|
start.length = 0;
|
|
185
|
-
start.push(
|
|
199
|
+
start.push(...leading, ...rest);
|
|
186
200
|
}
|
|
187
201
|
|
|
188
202
|
return { start, end };
|
package/src/adapter/pinning.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { ViewStyle } from 'react-native';
|
|
2
2
|
import type { ColumnPinningState } from '@tanstack/react-table';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DETAIL_PANEL_TOGGLE_ID,
|
|
5
|
+
SELECT_COLUMN_ID,
|
|
6
|
+
type GridColDef,
|
|
7
|
+
type GridValidRowModel,
|
|
8
|
+
} from '../types';
|
|
4
9
|
|
|
5
10
|
export const DEFAULT_PINNED_COLUMN_WIDTH = 100;
|
|
6
11
|
export const SELECT_COLUMN_WIDTH = 56;
|
|
12
|
+
export const DETAIL_PANEL_TOGGLE_WIDTH = 48;
|
|
7
13
|
|
|
8
14
|
export type ColumnPinEdge = 'start' | 'end';
|
|
9
15
|
|
|
@@ -22,6 +28,9 @@ export function pinnedColumnPixelWidth<R extends GridValidRowModel>(
|
|
|
22
28
|
if (columnId === SELECT_COLUMN_ID) {
|
|
23
29
|
return SELECT_COLUMN_WIDTH;
|
|
24
30
|
}
|
|
31
|
+
if (columnId === DETAIL_PANEL_TOGGLE_ID) {
|
|
32
|
+
return DETAIL_PANEL_TOGGLE_WIDTH;
|
|
33
|
+
}
|
|
25
34
|
return Math.max(col?.width ?? DEFAULT_PINNED_COLUMN_WIDTH, col?.minWidth ?? 0);
|
|
26
35
|
}
|
|
27
36
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SELECT_COLUMN_ID } from '../types';
|
|
1
|
+
import { DETAIL_PANEL_TOGGLE_ID, SELECT_COLUMN_ID } from '../types';
|
|
2
2
|
|
|
3
3
|
export function createSelectColumn() {
|
|
4
4
|
return {
|
|
@@ -10,3 +10,14 @@ export function createSelectColumn() {
|
|
|
10
10
|
size: 48,
|
|
11
11
|
} as const;
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
export function createDetailPanelToggleColumn() {
|
|
15
|
+
return {
|
|
16
|
+
id: DETAIL_PANEL_TOGGLE_ID,
|
|
17
|
+
header: DETAIL_PANEL_TOGGLE_ID,
|
|
18
|
+
enableSorting: false,
|
|
19
|
+
enableColumnFilter: false,
|
|
20
|
+
enableGlobalFilter: false,
|
|
21
|
+
size: 48,
|
|
22
|
+
} as const;
|
|
23
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
export interface GridTreeIndex<R> {
|
|
2
|
+
roots: R[];
|
|
3
|
+
childrenOf: Map<R, R[]>;
|
|
4
|
+
rowOf: Map<string, R>;
|
|
5
|
+
parentKeyOf: Map<string, string>;
|
|
6
|
+
childKeysOf: Map<string, string[]>;
|
|
7
|
+
depthOf: Map<string, number>;
|
|
8
|
+
/** Deepest rows first, so a parent is visited after all of its descendants. */
|
|
9
|
+
keysDeepestFirst: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type GridTreeSelectionStatus = 'all' | 'some' | 'none';
|
|
13
|
+
|
|
14
|
+
/** The shape TanStack rows share; filtered and sorted rows carry their filtered `subRows`. */
|
|
15
|
+
export interface GridTreeRowLike {
|
|
16
|
+
id: string;
|
|
17
|
+
subRows: readonly GridTreeRowLike[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const pathKey = (path: readonly string[]) => JSON.stringify(path);
|
|
21
|
+
|
|
22
|
+
const warnedDuplicatePaths = new Set<string>();
|
|
23
|
+
|
|
24
|
+
export function buildTreeIndex<R>(
|
|
25
|
+
rows: readonly R[],
|
|
26
|
+
getTreeDataPath: (row: R) => readonly string[],
|
|
27
|
+
keyOf: (row: R) => string,
|
|
28
|
+
): GridTreeIndex<R> {
|
|
29
|
+
const byPath = new Map<string, R>();
|
|
30
|
+
const paths = new Map<R, readonly string[]>();
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
const path = getTreeDataPath(row);
|
|
33
|
+
const key = pathKey(path);
|
|
34
|
+
if (__DEV__ && byPath.has(key) && !warnedDuplicatePaths.has(key)) {
|
|
35
|
+
warnedDuplicatePaths.add(key);
|
|
36
|
+
console.warn(`[DataGrid] Duplicate tree data path ${key}. Paths must be unique.`);
|
|
37
|
+
}
|
|
38
|
+
byPath.set(key, row);
|
|
39
|
+
paths.set(row, path);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const roots: R[] = [];
|
|
43
|
+
const childrenOf = new Map<R, R[]>();
|
|
44
|
+
const rowOf = new Map<string, R>();
|
|
45
|
+
const parentKeyOf = new Map<string, string>();
|
|
46
|
+
const childKeysOf = new Map<string, string[]>();
|
|
47
|
+
|
|
48
|
+
for (const row of rows) {
|
|
49
|
+
const path = paths.get(row) ?? [];
|
|
50
|
+
const key = keyOf(row);
|
|
51
|
+
rowOf.set(key, row);
|
|
52
|
+
let parent: R | undefined;
|
|
53
|
+
for (let length = path.length - 1; length > 0 && parent === undefined; length -= 1) {
|
|
54
|
+
parent = byPath.get(pathKey(path.slice(0, length)));
|
|
55
|
+
}
|
|
56
|
+
if (parent === undefined || parent === row) {
|
|
57
|
+
roots.push(row);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const parentKey = keyOf(parent);
|
|
61
|
+
parentKeyOf.set(key, parentKey);
|
|
62
|
+
const siblings = childrenOf.get(parent);
|
|
63
|
+
if (siblings) {
|
|
64
|
+
siblings.push(row);
|
|
65
|
+
} else {
|
|
66
|
+
childrenOf.set(parent, [row]);
|
|
67
|
+
}
|
|
68
|
+
const siblingKeys = childKeysOf.get(parentKey);
|
|
69
|
+
if (siblingKeys) {
|
|
70
|
+
siblingKeys.push(key);
|
|
71
|
+
} else {
|
|
72
|
+
childKeysOf.set(parentKey, [key]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const depthOf = new Map<string, number>();
|
|
77
|
+
const breadthFirst = roots.map((row) => keyOf(row));
|
|
78
|
+
for (const key of breadthFirst) {
|
|
79
|
+
depthOf.set(key, 0);
|
|
80
|
+
}
|
|
81
|
+
// The array iterator also visits children pushed during the loop.
|
|
82
|
+
for (const key of breadthFirst) {
|
|
83
|
+
for (const child of childKeysOf.get(key) ?? []) {
|
|
84
|
+
depthOf.set(child, (depthOf.get(key) ?? 0) + 1);
|
|
85
|
+
breadthFirst.push(child);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
roots,
|
|
91
|
+
childrenOf,
|
|
92
|
+
rowOf,
|
|
93
|
+
parentKeyOf,
|
|
94
|
+
childKeysOf,
|
|
95
|
+
depthOf,
|
|
96
|
+
keysDeepestFirst: breadthFirst.reverse(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* With `propagateParents`, a parent is `all` exactly when every child is `all`, whatever the model
|
|
102
|
+
* says about the parent itself. Without it, a parent's own selection decides, and partly selected
|
|
103
|
+
* descendants show as `some`.
|
|
104
|
+
*/
|
|
105
|
+
export function computeTreeSelection<R>(
|
|
106
|
+
index: GridTreeIndex<R>,
|
|
107
|
+
selected: ReadonlySet<string>,
|
|
108
|
+
propagateParents: boolean,
|
|
109
|
+
): Map<string, GridTreeSelectionStatus> {
|
|
110
|
+
const status = new Map<string, GridTreeSelectionStatus>();
|
|
111
|
+
for (const key of index.keysDeepestFirst) {
|
|
112
|
+
const isSelected = selected.has(key);
|
|
113
|
+
const children = index.childKeysOf.get(key);
|
|
114
|
+
if (!children?.length) {
|
|
115
|
+
status.set(key, isSelected ? 'all' : 'none');
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
let all = true;
|
|
119
|
+
let some = false;
|
|
120
|
+
for (const child of children) {
|
|
121
|
+
const childStatus = status.get(child) ?? 'none';
|
|
122
|
+
if (childStatus !== 'all') {
|
|
123
|
+
all = false;
|
|
124
|
+
}
|
|
125
|
+
if (childStatus !== 'none') {
|
|
126
|
+
some = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (propagateParents) {
|
|
130
|
+
status.set(key, all ? 'all' : some || isSelected ? 'some' : 'none');
|
|
131
|
+
} else {
|
|
132
|
+
status.set(key, isSelected ? 'all' : some ? 'some' : 'none');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return status;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function applyTreeSelection<R>(
|
|
139
|
+
index: GridTreeIndex<R>,
|
|
140
|
+
selected: ReadonlySet<string>,
|
|
141
|
+
targets: readonly string[],
|
|
142
|
+
value: boolean,
|
|
143
|
+
propagateParents: boolean,
|
|
144
|
+
): Set<string> {
|
|
145
|
+
const next = new Set(selected);
|
|
146
|
+
for (const key of targets) {
|
|
147
|
+
if (value) {
|
|
148
|
+
next.add(key);
|
|
149
|
+
} else {
|
|
150
|
+
next.delete(key);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (propagateParents) {
|
|
154
|
+
for (const key of index.keysDeepestFirst) {
|
|
155
|
+
const children = index.childKeysOf.get(key);
|
|
156
|
+
if (!children?.length) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (children.every((child) => next.has(child))) {
|
|
160
|
+
next.add(key);
|
|
161
|
+
} else {
|
|
162
|
+
next.delete(key);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return next;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Rows a toggle writes (`targets`) and the rows whose state decides the toggle direction
|
|
171
|
+
* (`units`). With `descendants`, a row's filtered sub-rows are included; a row whose children were
|
|
172
|
+
* all filtered out takes its whole subtree, so a matching group can still be picked.
|
|
173
|
+
*/
|
|
174
|
+
export function selectionScope<R>(
|
|
175
|
+
rows: readonly GridTreeRowLike[],
|
|
176
|
+
index: GridTreeIndex<R>,
|
|
177
|
+
descendants: boolean,
|
|
178
|
+
): { targets: string[]; units: string[] } {
|
|
179
|
+
const targets: string[] = [];
|
|
180
|
+
const units: string[] = [];
|
|
181
|
+
const visitFullSubtree = (key: string) => {
|
|
182
|
+
targets.push(key);
|
|
183
|
+
const children = index.childKeysOf.get(key);
|
|
184
|
+
if (!children?.length) {
|
|
185
|
+
units.push(key);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
children.forEach(visitFullSubtree);
|
|
189
|
+
};
|
|
190
|
+
const visit = (row: GridTreeRowLike) => {
|
|
191
|
+
if (!descendants) {
|
|
192
|
+
targets.push(row.id);
|
|
193
|
+
units.push(row.id);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (row.subRows.length === 0) {
|
|
197
|
+
visitFullSubtree(row.id);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
targets.push(row.id);
|
|
201
|
+
row.subRows.forEach(visit);
|
|
202
|
+
};
|
|
203
|
+
rows.forEach(visit);
|
|
204
|
+
return { targets, units };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Keeps every row that matches and every ancestor of a match. */
|
|
208
|
+
export function keepMatchesWithAncestors<R>(
|
|
209
|
+
rows: readonly R[],
|
|
210
|
+
index: GridTreeIndex<R>,
|
|
211
|
+
keyOf: (row: R) => string,
|
|
212
|
+
matches: (row: R) => boolean,
|
|
213
|
+
): R[] {
|
|
214
|
+
const keep = new Set<string>();
|
|
215
|
+
for (const row of rows) {
|
|
216
|
+
if (!matches(row)) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
for (let key: string | undefined = keyOf(row); key !== undefined; ) {
|
|
220
|
+
if (keep.has(key)) {
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
keep.add(key);
|
|
224
|
+
key = index.parentKeyOf.get(key);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return rows.filter((row) => keep.has(keyOf(row)));
|
|
228
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type {
|
|
|
13
13
|
GridBaseColDef,
|
|
14
14
|
GridColDef,
|
|
15
15
|
GridColType,
|
|
16
|
+
GridColumnVisibilityModel,
|
|
16
17
|
GridFeatureMode,
|
|
17
18
|
GridFilterItem,
|
|
18
19
|
GridFilterModel,
|
|
@@ -22,12 +23,15 @@ export type {
|
|
|
22
23
|
GridPinnedColumnFields,
|
|
23
24
|
GridRenderCellParams,
|
|
24
25
|
GridRowId,
|
|
26
|
+
GridRowParams,
|
|
25
27
|
GridRowSelectionModel,
|
|
28
|
+
GridRowSelectionPropagation,
|
|
26
29
|
GridRowsProp,
|
|
27
30
|
GridSingleSelectColDef,
|
|
28
31
|
GridSortDirection,
|
|
29
32
|
GridSortItem,
|
|
30
33
|
GridSortModel,
|
|
34
|
+
GridTreeNodeParams,
|
|
31
35
|
GridValidRowModel,
|
|
32
36
|
GridValueOption,
|
|
33
37
|
} from './types';
|