@dxos/react-ui-table 0.8.4-main.67995b8 → 0.8.4-main.6fa680abb7

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 (158) hide show
  1. package/dist/lib/browser/chunk-BF54SSNL.mjs +70 -0
  2. package/dist/lib/browser/chunk-BF54SSNL.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-PTHMYAAM.mjs +1751 -0
  4. package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1185 -1140
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +31 -38
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types/index.mjs +3 -3
  11. package/dist/lib/node-esm/chunk-GI72MQSG.mjs +1753 -0
  12. package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs +71 -0
  14. package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1185 -1140
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +31 -38
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +2 -3
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +3 -3
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +7 -7
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +37 -3
  28. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Relations.stories.d.ts +111 -0
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  32. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.d.ts +16 -12
  34. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.stories.d.ts +108 -4
  36. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  38. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +13 -9
  40. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +11 -18
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  46. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +5 -5
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +23 -16
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +2 -1
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +6 -6
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  56. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useTableModel.d.ts +10 -10
  58. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts +5 -8
  60. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  61. package/dist/types/src/model/selection-model.d.ts +17 -8
  62. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-model.d.ts +112 -30
  64. package/dist/types/src/model/table-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-presentation.d.ts +5 -2
  66. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  67. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  68. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  69. package/dist/types/src/testing/useTestTableModel.d.ts +18 -34
  70. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +4 -4
  72. package/dist/types/src/translations.d.ts.map +1 -1
  73. package/dist/types/src/types/Table.d.ts +26 -0
  74. package/dist/types/src/types/Table.d.ts.map +1 -0
  75. package/dist/types/src/types/index.d.ts +1 -1
  76. package/dist/types/src/types/index.d.ts.map +1 -1
  77. package/dist/types/src/util/dynamic-table.d.ts +14 -12
  78. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  79. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  80. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  81. package/dist/types/src/util/index.d.ts +2 -1
  82. package/dist/types/src/util/index.d.ts.map +1 -1
  83. package/dist/types/src/util/query-ast.d.ts +7 -0
  84. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  85. package/dist/types/src/util/schema.d.ts +1 -1
  86. package/dist/types/src/util/schema.d.ts.map +1 -1
  87. package/dist/types/src/util/sort.d.ts +12 -0
  88. package/dist/types/src/util/sort.d.ts.map +1 -0
  89. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  90. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  91. package/dist/types/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +65 -64
  93. package/src/components/Table/ColumnActionsMenu.tsx +11 -7
  94. package/src/components/Table/ColumnSettings.tsx +9 -10
  95. package/src/components/Table/DynamicTable.stories.tsx +28 -24
  96. package/src/components/Table/DynamicTable.tsx +34 -28
  97. package/src/components/Table/Relations.stories.tsx +240 -0
  98. package/src/components/Table/RowActionsMenu.tsx +26 -17
  99. package/src/components/Table/Table.stories.tsx +148 -118
  100. package/src/components/Table/Table.tsx +347 -297
  101. package/src/components/TableCellEditor/CellValidationMessage.tsx +6 -6
  102. package/src/components/TableCellEditor/FormCellEditor.tsx +134 -40
  103. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +31 -66
  104. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -238
  105. package/src/components/TableCellEditor/index.ts +1 -0
  106. package/src/components/TableToolbar/TableToolbar.tsx +54 -38
  107. package/src/components/testing.ts +39 -13
  108. package/src/hooks/index.ts +2 -1
  109. package/src/hooks/useAddRow.ts +14 -14
  110. package/src/hooks/useProjectionModel.ts +39 -0
  111. package/src/hooks/useTableModel.ts +36 -34
  112. package/src/model/modal-controller.ts +24 -41
  113. package/src/model/selection-model.ts +75 -43
  114. package/src/model/table-model.test.ts +92 -55
  115. package/src/model/table-model.ts +453 -175
  116. package/src/model/table-presentation.test.ts +70 -58
  117. package/src/model/table-presentation.ts +111 -64
  118. package/src/playwright/TableManager.ts +13 -6
  119. package/src/playwright/playwright.config.ts +1 -1
  120. package/src/playwright/smoke.spec.ts +171 -33
  121. package/src/testing/useTestTableModel.ts +46 -35
  122. package/src/translations.ts +3 -3
  123. package/src/types/Table.ts +79 -0
  124. package/src/types/index.ts +1 -1
  125. package/src/util/dynamic-table.test.ts +70 -0
  126. package/src/util/dynamic-table.ts +46 -33
  127. package/src/util/index.ts +2 -1
  128. package/src/util/query-ast.ts +22 -0
  129. package/src/util/schema.ts +4 -4
  130. package/src/util/sort.ts +90 -0
  131. package/src/util/table-buttons.ts +4 -3
  132. package/src/util/table-controls.ts +6 -3
  133. package/dist/lib/browser/chunk-5XLSHLOG.mjs +0 -1751
  134. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  135. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  136. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  137. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  138. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  139. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs +0 -1752
  140. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  141. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  142. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  143. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  144. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  145. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  146. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  147. package/dist/types/src/model/table-sorting.d.ts +0 -53
  148. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  149. package/dist/types/src/types/schema.d.ts +0 -8
  150. package/dist/types/src/types/schema.d.ts.map +0 -1
  151. package/dist/types/src/util/table.d.ts +0 -11
  152. package/dist/types/src/util/table.d.ts.map +0 -1
  153. package/src/components/Table/CreateRefPanel.tsx +0 -86
  154. package/src/components/Table/relations.stories.tsx +0 -116
  155. package/src/components/TableCellEditor/extension.ts +0 -61
  156. package/src/model/table-sorting.ts +0 -216
  157. package/src/types/schema.ts +0 -16
  158. package/src/util/table.ts +0 -49
@@ -2,414 +2,464 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Schema } from 'effect/Schema';
5
+ import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
+ import * as String from 'effect/String';
6
7
  import React, {
8
+ type JSX,
7
9
  type MouseEvent,
8
10
  type PropsWithChildren,
11
+ type Ref,
9
12
  type WheelEvent,
10
13
  forwardRef,
11
14
  useCallback,
15
+ useContext,
16
+ useEffect,
12
17
  useImperativeHandle,
13
- useState,
14
18
  useMemo,
15
- useEffect,
19
+ useState,
16
20
  } from 'react';
17
21
 
18
- import { type Client } from '@dxos/client';
19
- import { Filter } from '@dxos/echo';
20
- import { getValue } from '@dxos/echo-schema';
21
- import { invariant } from '@dxos/invariant';
22
- // TODO(wittjosiah): Remove dependency on react-client.
23
- import { getSpace } from '@dxos/react-client/echo';
24
- import { useTranslation } from '@dxos/react-ui';
22
+ import { type Type } from '@dxos/echo';
23
+ import { log } from '@dxos/log';
25
24
  import { useAttention } from '@dxos/react-ui-attention';
26
25
  import {
27
- closestCell,
26
+ type DxGridAxisMeta,
28
27
  type DxGridElement,
29
- type DxGridPosition,
30
- type GridContentProps,
31
- Grid,
32
28
  type DxGridPlane,
33
29
  type DxGridPlaneRange,
34
- gridSeparatorInlineEnd,
30
+ type DxGridPosition,
31
+ Grid,
32
+ type GridContentProps,
33
+ closestCell,
35
34
  gridSeparatorBlockEnd,
35
+ gridSeparatorInlineEnd,
36
36
  } from '@dxos/react-ui-grid';
37
- import { mx } from '@dxos/react-ui-theme';
38
- import { isNotFalsy, safeParseInt } from '@dxos/util';
37
+ import { DxEditRequest } from '@dxos/react-ui-grid';
38
+ import { composableProps, mx } from '@dxos/ui-theme';
39
+ import { type ComposableProps } from '@dxos/ui-types';
40
+
41
+ import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
42
+ import { tableButtons, tableControls } from '../../util';
43
+ import { type OnCreateHandler, type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
44
+ import { TableToolbar } from '../TableToolbar';
39
45
 
40
46
  import { ColumnActionsMenu } from './ColumnActionsMenu';
41
47
  import { ColumnSettings } from './ColumnSettings';
42
- import { CreateRefPanel } from './CreateRefPanel';
43
48
  import { RowActionsMenu } from './RowActionsMenu';
44
- import { ModalController, type TableModel, type TablePresentation } from '../../model';
45
- import { translationKey } from '../../translations';
46
- import { tableButtons, tableControls } from '../../util';
47
- import { createOption, TableValueEditor, type TableCellEditorProps } from '../TableCellEditor';
49
+
50
+ const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
51
+ const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
52
+ const emptyColumnMeta = Atom.make<DxGridAxisMeta>({ grid: {} });
48
53
 
49
54
  //
50
55
  // Table.Root
51
56
  //
52
57
 
53
- export type TableRootProps = PropsWithChildren<{ role?: string }>;
58
+ export type TableRootProps = PropsWithChildren;
54
59
 
55
- const TableRoot = ({ children, role = 'article' }: TableRootProps) => {
56
- return (
57
- <div
58
- role='none'
59
- className={mx(
60
- 'relative !border-separator [&_.dx-grid]:max-is-[--dx-grid-content-inline-size] [&_.dx-grid]:max-bs-[--dx-grid-content-block-size]',
61
- role === 'card--popover' && 'popover-card-height',
62
- role === 'section' && 'attention-surface',
63
- role === 'card--intrinsic' && '[&_.dx-grid]:bs-[--dx-grid-content-block-size]',
64
- ['card--popover', 'section', 'card--extrinsic'].includes(role) && 'overflow-hidden',
65
- ['article', 'slide'].includes(role) && 'flex flex-col [&_.dx-grid]:grow [&_.dx-grid]:bs-0',
66
- )}
67
- >
68
- {children}
69
- </div>
70
- );
60
+ const TableRoot = ({ children }: TableRootProps) => {
61
+ return <>{children}</>;
71
62
  };
72
63
 
73
64
  //
74
65
  // Table.Main
75
66
  //
76
67
 
68
+ const emptyRowsAtom = Atom.make<unknown[]>([]);
69
+ const emptyCellUpdateAtom = Atom.make<number>(0);
70
+
77
71
  export type TableController = {
78
72
  update?: (cell?: DxGridPosition) => void;
73
+ handleInsertRowResult?: (insertRowResult?: InsertRowResult) => void;
79
74
  };
80
75
 
81
- export type TableMainProps = {
76
+ export type TableMainProps<T extends Type.AnyEntity = Type.AnyEntity> = ComposableProps<HTMLDivElement> & {
77
+ schema?: T;
82
78
  model?: TableModel;
83
79
  presentation?: TablePresentation;
84
- schema?: Schema.AnyNoContext;
85
- client?: Client;
86
- // TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
80
+ // TODO(burdon): Factor out attention.
87
81
  ignoreAttention?: boolean;
82
+ onCreate?: OnCreateHandler;
88
83
  onRowClick?: (row: any) => void;
84
+ testId?: string;
89
85
  };
90
86
 
91
- const TableMain = forwardRef<TableController, TableMainProps>(
92
- ({ model, presentation, ignoreAttention, schema, client, onRowClick }, forwardedRef) => {
93
- const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
94
- const { hasAttention } = useAttention(model?.id ?? 'table');
95
- const { t } = useTranslation(translationKey);
96
- const modals = useMemo(() => new ModalController(), []);
97
-
98
- const draftRowCount = model?.getDraftRowCount() ?? 0;
99
-
100
- const frozen = useMemo(() => {
101
- const noActionColumn =
102
- model?.features.dataEditable === false &&
103
- model?.features.schemaEditable === false &&
104
- model.rowActions.length === 0;
105
-
106
- return {
107
- frozenRowsStart: 1,
108
- frozenRowsEnd: draftRowCount,
109
- frozenColsStart: model?.features.selection.enabled ? 1 : 0,
110
- frozenColsEnd: noActionColumn ? 0 : 1,
111
- };
112
- }, [model, draftRowCount]);
113
-
114
- const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
115
- (range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
116
- [presentation],
117
- );
118
-
119
- useEffect(() => {
120
- if (!presentation || !dxGrid) {
121
- return;
87
+ const TableMainInner = <T extends Type.AnyEntity = Type.AnyEntity>(
88
+ { schema, model, presentation, ignoreAttention, onCreate, onRowClick, testId, ...props }: TableMainProps<T>,
89
+ forwardedRef: Ref<TableController>,
90
+ ) => {
91
+ const registry = useContext(RegistryContext);
92
+ const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
93
+ const { hasAttention } = useAttention(model?.id ?? 'table');
94
+ const modals = useMemo(() => new ModalController(registry), [registry]);
95
+ const columnMeta = useAtomValue(model?.columnMeta ?? emptyColumnMeta);
96
+ // Subscribe to rows atom to trigger re-render when rows change.
97
+ const rows = useAtomValue(model?.rows ?? emptyRowsAtom);
98
+ // Subscribe to cell update counter to trigger re-render when individual cells change.
99
+ const cellUpdateCounter = useAtomValue(model?.cellUpdate ?? emptyCellUpdateAtom);
100
+ // Derive column count from columnMeta (reactive) with fallback.
101
+ const columnCount = Object.keys(columnMeta.grid).length || model?.projection.getFields().length || 0;
102
+
103
+ const draftRowCount = model?.getDraftRowCount() ?? 0;
104
+
105
+ const frozen = useMemo(() => {
106
+ const noActionColumn =
107
+ model?.features.dataEditable === false &&
108
+ model?.features.schemaEditable === false &&
109
+ model.rowActions.length === 0;
110
+
111
+ return {
112
+ frozenRowsStart: 1,
113
+ frozenRowsEnd: Math.max(1, draftRowCount),
114
+ frozenColsStart: model?.features.selection.enabled ? 1 : 0,
115
+ frozenColsEnd: noActionColumn ? 0 : 1,
116
+ };
117
+ }, [model, draftRowCount]);
118
+
119
+ const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
120
+ (range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
121
+ [presentation],
122
+ );
123
+
124
+ useEffect(() => {
125
+ if (!dxGrid || !presentation) {
126
+ return;
127
+ }
128
+
129
+ dxGrid.getCells = getCells;
130
+ }, [dxGrid, presentation, getCells]);
131
+
132
+ // Trigger grid update when rows or cell values change.
133
+ useEffect(() => {
134
+ if (!dxGrid) {
135
+ return;
136
+ }
137
+
138
+ dxGrid.updateCells(true);
139
+ dxGrid.requestUpdate();
140
+ }, [dxGrid, rows, cellUpdateCounter]);
141
+
142
+ const handleInsertRowResult = useCallback(
143
+ (insertResult?: InsertRowResult) => {
144
+ if (insertResult === 'draft') {
145
+ requestAnimationFrame(() => {
146
+ dxGrid?.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
147
+ dxGrid?.refocus();
148
+ });
149
+ } else {
150
+ requestAnimationFrame(() => {
151
+ dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
152
+ dxGrid?.refocus();
153
+ });
122
154
  }
123
- dxGrid.getCells = getCells;
124
- }, [presentation, dxGrid, getCells]);
125
-
126
- /**
127
- * Provides an external controller that can be called to repaint the table.
128
- */
129
- useImperativeHandle<TableController, TableController>(forwardedRef, () => {
130
- if (!presentation || !dxGrid) {
131
- return {};
155
+ },
156
+ [model, dxGrid],
157
+ );
158
+
159
+ const handleSave = useCallback(() => {
160
+ dxGrid?.updateCells(true);
161
+ dxGrid?.requestUpdate();
162
+ }, [dxGrid]);
163
+
164
+ const handleSaveDraftRow = useCallback(
165
+ (rowIndex = 0) => {
166
+ if (model && dxGrid) {
167
+ const didCommitSuccessfully = model.commitDraftRow(rowIndex);
168
+ if (didCommitSuccessfully) {
169
+ requestAnimationFrame(() => {
170
+ dxGrid.scrollToEndRow();
171
+ });
172
+ }
132
173
  }
174
+ },
175
+ [model, dxGrid],
176
+ );
133
177
 
134
- return {
135
- update: (cell) => {
136
- if (cell) {
137
- dxGrid.updateIfWithinBounds(cell, true);
138
- } else {
139
- dxGrid.updateCells(true);
140
- dxGrid.requestUpdate();
141
- }
142
- },
143
- };
144
- }, [presentation, dxGrid]);
145
-
146
- const handleGridClick = useCallback(
147
- (event: MouseEvent) => {
148
- const rowIndex = safeParseInt((event.target as HTMLElement).closest('[aria-rowindex]')?.ariaRowIndex ?? '');
149
- if (rowIndex != null) {
150
- if (onRowClick) {
178
+ const handleGridClick = useCallback(
179
+ (event: MouseEvent) => {
180
+ const cell = closestCell(event.target as HTMLElement);
181
+ if (cell) {
182
+ const { row: rowIndex, plane } = cell;
183
+ if (onRowClick) {
184
+ if (plane === 'grid') {
151
185
  const row = model?.getRowAt(rowIndex);
152
186
  row && onRowClick(row);
153
- }
154
-
155
- if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
156
- model.selection.toggleSelectionForRowIndex(rowIndex);
187
+ } else {
188
+ onRowClick(cell);
157
189
  }
158
190
  }
159
191
 
160
- if (!modals) {
161
- return;
192
+ if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
193
+ model.selection.toggleSelectionForRowIndex(rowIndex);
162
194
  }
195
+ }
163
196
 
164
- const buttonSelector = Object.values(tableButtons)
165
- .map((button) => `button[${button.attr}]`)
166
- .join(',');
197
+ if (!modals) {
198
+ return;
199
+ }
167
200
 
168
- const tableButtonElement = (event.target as HTMLElement).closest(buttonSelector) as HTMLElement;
169
- if (tableButtonElement) {
170
- const button = Object.values(tableButtons).find((btn) => tableButtonElement.hasAttribute(btn.attr));
171
- if (!button) {
172
- return;
173
- }
201
+ const buttonSelector = Object.values(tableButtons)
202
+ .map((button) => `button[${button.attr}]`)
203
+ .join(',');
174
204
 
175
- modals.setTrigger(tableButtonElement);
176
- const data = button.getData(tableButtonElement);
177
- switch (data.type) {
178
- case 'rowMenu': {
179
- modals.showRowMenu(data.rowIndex);
180
- break;
181
- }
182
- case 'columnSettings': {
183
- modals.showColumnMenu(data.fieldId);
184
- break;
185
- }
186
- case 'newColumn': {
187
- modals.showColumnCreator();
188
- break;
189
- }
190
- case 'sort': {
191
- model?.sorting?.toggleSort(data.fieldId);
192
- break;
193
- }
194
- case 'saveDraftRow': {
195
- if (model) {
196
- const didCommitSuccessfully = model.commitDraftRow(data.rowIndex);
197
- if (dxGrid && didCommitSuccessfully) {
198
- requestAnimationFrame(() => {
199
- dxGrid.scrollToEndRow();
200
- });
201
- }
202
- }
203
- break;
204
- }
205
- }
205
+ const tableButtonElement = (event.target as HTMLElement).closest(buttonSelector) as HTMLElement;
206
+ if (tableButtonElement) {
207
+ const button = Object.values(tableButtons).find((btn) => tableButtonElement.hasAttribute(btn.attr));
208
+ if (!button) {
206
209
  return;
207
210
  }
208
211
 
209
- const controlSelector = Object.values(tableControls)
210
- .map((control) => `input[${control.attr}]`)
211
- .join(',');
212
-
213
- const controlElement = (event.target as HTMLElement).closest(controlSelector) as HTMLElement;
214
- if (controlElement) {
215
- const control = Object.values(tableControls).find((ctrl) => controlElement.hasAttribute(ctrl.attr));
216
- if (!control) {
217
- return;
212
+ modals.setTrigger(tableButtonElement);
213
+ const data = button.getData(tableButtonElement);
214
+ switch (data.type) {
215
+ case 'rowMenu': {
216
+ modals.showRowMenu(data.rowIndex);
217
+ break;
218
218
  }
219
-
220
- const data = control.getData(controlElement);
221
- switch (data.type) {
222
- case 'checkbox': {
223
- if (data.header) {
224
- model?.selection.setSelection(model.selection.allRowsSeleted.value ? 'none' : 'all');
225
- } else {
226
- model?.selection.toggleSelectionForRowIndex(data.rowIndex);
227
- }
228
- break;
229
- }
230
- case 'switch': {
231
- if (model) {
232
- model.updateCellData({ row: data.rowIndex, col: data.colIndex }, (value) => !value);
233
- }
234
- break;
235
- }
219
+ case 'columnSettings': {
220
+ modals.showColumnMenu(data.fieldId);
221
+ break;
222
+ }
223
+ case 'newColumn': {
224
+ modals.showColumnCreator();
225
+ break;
226
+ }
227
+ case 'sort': {
228
+ model?.toggleSort(data.fieldId);
229
+ break;
230
+ }
231
+ case 'saveDraftRow': {
232
+ handleSaveDraftRow();
233
+ break;
236
234
  }
237
235
  }
238
- },
239
- [model, modals, dxGrid],
240
- );
236
+ return;
237
+ }
241
238
 
242
- const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
243
- (increment, delta) => {
244
- if (dxGrid) {
245
- dxGrid.refocus(increment, delta);
246
- }
247
- },
248
- [dxGrid],
249
- );
239
+ const controlSelector = Object.values(tableControls)
240
+ .map((control) => `input[${control.attr}]`)
241
+ .join(',');
250
242
 
251
- const handleEnter = useCallback<NonNullable<TableCellEditorProps['onEnter']>>(
252
- (cell) => {
253
- if (!model?.features.dataEditable) {
243
+ const controlElement = (event.target as HTMLElement).closest(controlSelector) as HTMLElement;
244
+ if (controlElement) {
245
+ const control = Object.values(tableControls).find((ctrl) => controlElement.hasAttribute(ctrl.attr));
246
+ if (!control) {
254
247
  return;
255
248
  }
256
249
 
257
- // TODO(burdon): Insert row only if bottom row isn't completely blank already.
258
- if (model && cell.row === model.getRowCount() - 1) {
259
- model.insertRow();
260
- if (dxGrid) {
261
- requestAnimationFrame(() => {
262
- dxGrid?.scrollToRow(cell.row + 1);
263
- dxGrid?.refocus('row', 1);
264
- });
250
+ const data = control.getData(controlElement);
251
+ switch (data.type) {
252
+ case 'checkbox': {
253
+ if (data.header) {
254
+ model?.selection.setSelection(model.selection.allRowsSelected ? 'none' : 'all');
255
+ } else {
256
+ model?.selection.toggleSelectionForRowIndex(data.rowIndex);
257
+ }
258
+ break;
259
+ }
260
+ case 'switch': {
261
+ if (model) {
262
+ model.updateCellData({ row: data.rowIndex, col: data.colIndex }, (value) => !value);
263
+ }
264
+ break;
265
265
  }
266
266
  }
267
- },
268
- [model, dxGrid],
269
- );
267
+ }
268
+ },
269
+ [model, modals, dxGrid],
270
+ );
270
271
 
271
- const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
272
- (event) => {
273
- if (!model?.features.dataEditable) {
274
- return;
272
+ const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
273
+ (increment, delta, cell) => {
274
+ if (dxGrid && model) {
275
+ if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1 && increment !== 'col') {
276
+ handleInsertRowResult(draftRowCount < 1 ? model.insertRow() : 'final');
277
+ } else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
278
+ handleSaveDraftRow(cell.row);
279
+ handleInsertRowResult(model.insertRow());
280
+ } else {
281
+ dxGrid.refocus(increment, delta);
275
282
  }
283
+ }
284
+ },
285
+ [dxGrid, model],
286
+ );
276
287
 
277
- const cell = closestCell(event.target);
278
- if (!model || !cell) {
279
- return;
280
- }
288
+ const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
289
+ (event) => {
290
+ if (!model?.features.dataEditable) {
291
+ return;
292
+ }
293
+
294
+ const cell = closestCell(event.target);
295
+ if (!model || !cell) {
296
+ return;
297
+ }
281
298
 
299
+ // Handle Meta+C (Copy) and Meta+V (Paste) commands
300
+ if (event.metaKey || event.ctrlKey) {
282
301
  switch (event.key) {
283
- case 'Backspace':
284
- case 'Delete': {
302
+ case 'c': {
303
+ // Copy focused cell's text content to clipboard
285
304
  try {
286
- model.setCellData(cell, undefined);
305
+ const cellData = model.getCellData(cell);
306
+ const textContent = cellData?.toString() ?? '';
307
+ void navigator.clipboard.writeText(textContent);
287
308
  event.preventDefault();
288
- } catch {
289
- // Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
309
+ } catch (error) {
310
+ log.catch(error);
290
311
  }
291
312
  break;
292
313
  }
314
+ case 'v': {
315
+ // Paste clipboard content to focused cell
316
+ event.preventDefault();
317
+ void navigator.clipboard.readText().then((clipboardText) => {
318
+ try {
319
+ // Attempt to set the cell's content to clipboard content
320
+ model.setCellData(cell, String.trim(clipboardText).replace(/[\n\r]+/, ' '));
321
+ handleSave();
322
+ } catch {
323
+ // If validation fails, emit a DxEditRequest event with initialContent from clipboard
324
+ // TODO(thure): Should `dx-grid` expose a method like this?
325
+ const cellElement = (event.target as HTMLElement).closest(
326
+ '[data-dx-grid-action="cell"]',
327
+ ) as HTMLElement;
328
+ if (cellElement) {
329
+ const rect = cellElement.getBoundingClientRect();
330
+ const editRequest = new DxEditRequest({
331
+ cellIndex: `${cell.plane},${cell.col},${cell.row}`,
332
+ cellBox: {
333
+ insetInlineStart: rect.left,
334
+ insetBlockStart: rect.top,
335
+ inlineSize: rect.width,
336
+ blockSize: rect.height,
337
+ },
338
+ cellElement,
339
+ initialContent: clipboardText,
340
+ });
341
+ cellElement.dispatchEvent(editRequest);
342
+ }
343
+ }
344
+ });
345
+ break;
346
+ }
293
347
  }
294
- },
295
- [model],
296
- );
297
-
298
- const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
299
- (event) => {
300
- if (event.axis === 'col') {
301
- const columnIndex = parseInt(event.index, 10);
302
- model?.setColumnWidth(columnIndex, event.size);
303
- }
304
- },
305
- [model],
306
- );
348
+ }
307
349
 
308
- const handleWheel = useCallback(
309
- (event: WheelEvent) => {
310
- if (!ignoreAttention && !hasAttention) {
311
- event.stopPropagation();
350
+ switch (event.key) {
351
+ case 'Backspace':
352
+ case 'Delete': {
353
+ try {
354
+ model.setCellData(cell, undefined);
355
+ event.preventDefault();
356
+ handleSave();
357
+ } catch {
358
+ // Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
359
+ }
360
+ break;
312
361
  }
313
- },
314
- [hasAttention, ignoreAttention],
315
- );
362
+ }
363
+ },
364
+ [model, dxGrid, handleSave],
365
+ );
316
366
 
317
- const handleNewColumn = useCallback(() => {
318
- const columns = model?.getColumnCount();
319
- if (dxGrid && columns) {
320
- dxGrid.scrollToColumn(columns - 1);
367
+ const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
368
+ (event) => {
369
+ if (event.axis === 'col') {
370
+ const columnIndex = parseInt(event.index, 10);
371
+ model?.setColumnWidth(columnIndex, event.size);
321
372
  }
322
- }, [model, dxGrid]);
323
-
324
- // TODO(burdon): Factor out?
325
- // TODO(burdon): Generalize to handle other value types (e.g., enums).
326
- const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
327
- async ({ field, props }, text) => {
328
- if (model && props.referenceSchema && field.referencePath) {
329
- const space = getSpace(model.view);
330
- invariant(space);
331
-
332
- let schema;
333
- if (client) {
334
- schema = client.graph.schemaRegistry.getSchema(props.referenceSchema);
335
- }
336
- if (!schema) {
337
- schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
338
- }
373
+ },
374
+ [model],
375
+ );
339
376
 
340
- if (schema) {
341
- const { objects } = await space.db.query(Filter.type(schema)).run();
342
- const options = objects
343
- .map((obj) => {
344
- const value = getValue(obj, field.referencePath!);
345
- if (!value || typeof value !== 'string') {
346
- return undefined;
347
- }
377
+ const handleWheel = useCallback(
378
+ (event: WheelEvent) => {
379
+ if (!ignoreAttention && !hasAttention) {
380
+ event.stopPropagation();
381
+ }
382
+ },
383
+ [hasAttention, ignoreAttention],
384
+ );
348
385
 
349
- return {
350
- label: value,
351
- data: obj,
352
- };
353
- })
354
- .filter(isNotFalsy);
355
-
356
- return [
357
- ...options,
358
- {
359
- label: t('create new object label', { text }),
360
- data: createOption(text),
361
- },
362
- ];
363
- }
364
- }
386
+ const handleNewColumn = useCallback(() => {
387
+ const columns = model?.getColumnCount();
388
+ if (dxGrid && columns) {
389
+ dxGrid.scrollToColumn(columns - 1);
390
+ }
391
+ }, [model, dxGrid]);
392
+
393
+ /**
394
+ * Provides an external controller that can be called to repaint the table.
395
+ */
396
+ useImperativeHandle<TableController, TableController>(forwardedRef, () => {
397
+ if (!dxGrid || !presentation) {
398
+ return {};
399
+ }
365
400
 
366
- return [];
401
+ return {
402
+ update: (cell) => {
403
+ if (cell) {
404
+ dxGrid.updateIfWithinBounds(cell, true);
405
+ } else {
406
+ dxGrid.updateCells(true);
407
+ dxGrid.requestUpdate();
408
+ }
367
409
  },
368
- [model, client, t],
369
- );
410
+ handleInsertRowResult,
411
+ };
412
+ }, [dxGrid, model, presentation]);
370
413
 
371
- const handleSave = useCallback(() => {
372
- dxGrid?.updateCells(true);
373
- }, [dxGrid]);
374
-
375
- if (!model || !modals) {
376
- return <span role='none' className='attention-surface' />;
377
- }
414
+ if (!model || !modals) {
415
+ return <span role='none' className='dx-attention-surface' />;
416
+ }
378
417
 
379
- return (
418
+ return (
419
+ <div {...composableProps(props, { role: 'none', className: 'dx-container relative' })}>
380
420
  <Grid.Root id={model.id ?? 'table-grid'}>
381
- <TableValueEditor
421
+ <TableValueEditor<T>
382
422
  model={model}
383
423
  modals={modals}
384
424
  schema={schema}
385
- onEnter={handleEnter}
386
425
  onFocus={handleFocus}
387
- onQuery={handleQuery}
388
426
  onSave={handleSave}
427
+ onCreate={onCreate}
389
428
  />
390
429
  <Grid.Content
391
- className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
430
+ className={mx('[--dx-grid-base:var(--base-surface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
392
431
  frozen={frozen}
393
- columns={model.columnMeta.value}
394
- limitRows={model.getRowCount() ?? 0}
395
- limitColumns={model.projection.fields.length}
432
+ columns={columnMeta}
433
+ columnDefault={columnDefault}
434
+ rowDefault={rowDefault}
435
+ limitRows={rows.length}
436
+ limitColumns={columnCount}
396
437
  overscroll='trap'
397
438
  onAxisResize={handleAxisResize}
398
439
  onClick={handleGridClick}
399
440
  onKeyDownCapture={handleKeyDown}
400
441
  onWheelCapture={handleWheel}
442
+ {...(testId && { 'data-testid': testId })}
401
443
  ref={setDxGrid}
402
444
  />
403
445
  <RowActionsMenu model={model} modals={modals} />
404
446
  <ColumnActionsMenu model={model} modals={modals} />
405
447
  <ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
406
- <CreateRefPanel model={model} modals={modals} />
407
448
  </Grid.Root>
408
- );
409
- },
410
- );
449
+ </div>
450
+ );
451
+ };
452
+
453
+ const TableMain = forwardRef(TableMainInner) as <T extends Type.AnyEntity = Type.AnyEntity>(
454
+ props: TableMainProps<T> & { ref?: Ref<TableController> },
455
+ ) => JSX.Element;
456
+
457
+ //
458
+ // Table
459
+ //
411
460
 
412
461
  export const Table = {
413
462
  Root: TableRoot,
414
463
  Main: TableMain,
464
+ Toolbar: TableToolbar,
415
465
  };