@dxos/react-ui-table 0.8.1-staging.5be625a → 0.8.1-staging.9eaf14f

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 (172) hide show
  1. package/dist/lib/browser/{chunk-5MKYB7NF.mjs → chunk-YWNRUF4J.mjs} +7 -3
  2. package/dist/lib/browser/{chunk-5MKYB7NF.mjs.map → chunk-YWNRUF4J.mjs.map} +3 -3
  3. package/dist/lib/browser/index.mjs +295 -97
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types/index.mjs +1 -1
  7. package/dist/lib/node/{chunk-NRFKBRPV.cjs → chunk-TGG6OZ54.cjs} +10 -6
  8. package/dist/lib/node/{chunk-NRFKBRPV.cjs.map → chunk-TGG6OZ54.cjs.map} +3 -3
  9. package/dist/lib/node/index.cjs +293 -93
  10. package/dist/lib/node/index.cjs.map +3 -3
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/types/index.cjs +3 -3
  13. package/dist/lib/node/types/index.cjs.map +1 -1
  14. package/dist/lib/node-esm/{chunk-SU4D55KI.mjs → chunk-KKLP54L3.mjs} +7 -3
  15. package/dist/lib/node-esm/{chunk-SU4D55KI.mjs.map → chunk-KKLP54L3.mjs.map} +3 -3
  16. package/dist/lib/node-esm/index.mjs +295 -97
  17. package/dist/lib/node-esm/index.mjs.map +3 -3
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/types/index.mjs +1 -1
  20. package/dist/types/src/components/Table/DynamicTable.d.ts +15 -10
  21. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +10 -0
  23. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -0
  24. package/dist/types/src/components/Table/Table.d.ts +1 -1
  25. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  26. package/dist/types/src/components/Table/Table.stories.d.ts +2 -3
  27. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/Table/relations.stories.d.ts +9 -0
  29. package/dist/types/src/components/Table/relations.stories.d.ts.map +1 -0
  30. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  31. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/testing.d.ts.map +1 -1
  33. package/dist/types/src/hooks/useTableModel.d.ts +6 -10
  34. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  35. package/dist/types/src/model/index.d.ts +1 -0
  36. package/dist/types/src/model/index.d.ts.map +1 -1
  37. package/dist/types/src/model/selection-model.d.ts +6 -3
  38. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  39. package/dist/types/src/model/table-model.d.ts +13 -14
  40. package/dist/types/src/model/table-model.d.ts.map +1 -1
  41. package/dist/types/src/model/table-presentation.d.ts +4 -4
  42. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  43. package/dist/types/src/model/table-sorting.d.ts +2 -2
  44. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  45. package/dist/types/src/types/table.d.ts +2 -1
  46. package/dist/types/src/types/table.d.ts.map +1 -1
  47. package/dist/types/src/util/dynamic-table.d.ts +21 -30
  48. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  49. package/dist/types/src/util/table-controls.d.ts +2 -2
  50. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  51. package/dist/types/src/util/table.d.ts.map +1 -1
  52. package/package.json +33 -32
  53. package/src/components/Table/DynamicTable.stories.tsx +155 -0
  54. package/src/components/Table/DynamicTable.tsx +46 -28
  55. package/src/components/Table/Table.stories.tsx +129 -125
  56. package/src/components/Table/Table.tsx +51 -26
  57. package/src/components/Table/relations.stories.tsx +113 -0
  58. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +8 -3
  59. package/src/components/TableCellEditor/TableCellEditor.tsx +102 -4
  60. package/src/components/testing.ts +0 -1
  61. package/src/hooks/useTableModel.ts +11 -11
  62. package/src/model/index.ts +1 -0
  63. package/src/model/selection-model.ts +12 -2
  64. package/src/model/table-model.test.ts +1 -1
  65. package/src/model/table-model.ts +27 -18
  66. package/src/model/table-presentation.test.ts +1 -1
  67. package/src/model/table-presentation.ts +65 -11
  68. package/src/model/table-sorting.ts +2 -2
  69. package/src/playwright/smoke.spec.ts +2 -3
  70. package/src/types/table.ts +6 -5
  71. package/src/util/dynamic-table.ts +57 -25
  72. package/src/util/table-controls.ts +4 -4
  73. package/src/util/table.ts +2 -1
  74. package/dist/lib/browser/deprecated/index.mjs +0 -1483
  75. package/dist/lib/browser/deprecated/index.mjs.map +0 -7
  76. package/dist/lib/node/deprecated/index.cjs +0 -1511
  77. package/dist/lib/node/deprecated/index.cjs.map +0 -7
  78. package/dist/lib/node-esm/deprecated/index.mjs +0 -1485
  79. package/dist/lib/node-esm/deprecated/index.mjs.map +0 -7
  80. package/dist/types/src/deprecated/components/Cell/Cell.d.ts +0 -12
  81. package/dist/types/src/deprecated/components/Cell/Cell.d.ts.map +0 -1
  82. package/dist/types/src/deprecated/components/Cell/CellCombobox.d.ts +0 -13
  83. package/dist/types/src/deprecated/components/Cell/CellCombobox.d.ts.map +0 -1
  84. package/dist/types/src/deprecated/components/Cell/index.d.ts +0 -2
  85. package/dist/types/src/deprecated/components/Cell/index.d.ts.map +0 -1
  86. package/dist/types/src/deprecated/components/ColumnMenu/ColumnMenu.d.ts +0 -23
  87. package/dist/types/src/deprecated/components/ColumnMenu/ColumnMenu.d.ts.map +0 -1
  88. package/dist/types/src/deprecated/components/ColumnMenu/ColumnMenu.stories.d.ts +0 -36
  89. package/dist/types/src/deprecated/components/ColumnMenu/ColumnMenu.stories.d.ts.map +0 -1
  90. package/dist/types/src/deprecated/components/ColumnMenu/ColumnSettings.d.ts +0 -15
  91. package/dist/types/src/deprecated/components/ColumnMenu/ColumnSettings.d.ts.map +0 -1
  92. package/dist/types/src/deprecated/components/ColumnMenu/ColumnSettings.stories.d.ts +0 -84
  93. package/dist/types/src/deprecated/components/ColumnMenu/ColumnSettings.stories.d.ts.map +0 -1
  94. package/dist/types/src/deprecated/components/ColumnMenu/index.d.ts +0 -3
  95. package/dist/types/src/deprecated/components/ColumnMenu/index.d.ts.map +0 -1
  96. package/dist/types/src/deprecated/components/Table/Table.d.ts +0 -20
  97. package/dist/types/src/deprecated/components/Table/Table.d.ts.map +0 -1
  98. package/dist/types/src/deprecated/components/Table/Table.stories.d.ts +0 -116
  99. package/dist/types/src/deprecated/components/Table/Table.stories.d.ts.map +0 -1
  100. package/dist/types/src/deprecated/components/Table/TableBody.d.ts +0 -12
  101. package/dist/types/src/deprecated/components/Table/TableBody.d.ts.map +0 -1
  102. package/dist/types/src/deprecated/components/Table/TableContext.d.ts +0 -13
  103. package/dist/types/src/deprecated/components/Table/TableContext.d.ts.map +0 -1
  104. package/dist/types/src/deprecated/components/Table/TableHead.d.ts +0 -9
  105. package/dist/types/src/deprecated/components/Table/TableHead.d.ts.map +0 -1
  106. package/dist/types/src/deprecated/components/Table/TableRootContext.d.ts +0 -17
  107. package/dist/types/src/deprecated/components/Table/TableRootContext.d.ts.map +0 -1
  108. package/dist/types/src/deprecated/components/Table/index.d.ts +0 -5
  109. package/dist/types/src/deprecated/components/Table/index.d.ts.map +0 -1
  110. package/dist/types/src/deprecated/components/Table/props.d.ts +0 -42
  111. package/dist/types/src/deprecated/components/Table/props.d.ts.map +0 -1
  112. package/dist/types/src/deprecated/components/index.d.ts +0 -4
  113. package/dist/types/src/deprecated/components/index.d.ts.map +0 -1
  114. package/dist/types/src/deprecated/helpers.d.ts +0 -91
  115. package/dist/types/src/deprecated/helpers.d.ts.map +0 -1
  116. package/dist/types/src/deprecated/hooks/index.d.ts +0 -5
  117. package/dist/types/src/deprecated/hooks/index.d.ts.map +0 -1
  118. package/dist/types/src/deprecated/hooks/useColumnSizing.d.ts +0 -12
  119. package/dist/types/src/deprecated/hooks/useColumnSizing.d.ts.map +0 -1
  120. package/dist/types/src/deprecated/hooks/useColumnSorting.d.ts +0 -15
  121. package/dist/types/src/deprecated/hooks/useColumnSorting.d.ts.map +0 -1
  122. package/dist/types/src/deprecated/hooks/usePinLastRow.d.ts +0 -3
  123. package/dist/types/src/deprecated/hooks/usePinLastRow.d.ts.map +0 -1
  124. package/dist/types/src/deprecated/hooks/useRowSelection.d.ts +0 -7
  125. package/dist/types/src/deprecated/hooks/useRowSelection.d.ts.map +0 -1
  126. package/dist/types/src/deprecated/index.d.ts +0 -8
  127. package/dist/types/src/deprecated/index.d.ts.map +0 -1
  128. package/dist/types/src/deprecated/schema/Schema.stories.d.ts +0 -11
  129. package/dist/types/src/deprecated/schema/Schema.stories.d.ts.map +0 -1
  130. package/dist/types/src/deprecated/schema/column-utils.d.ts +0 -7
  131. package/dist/types/src/deprecated/schema/column-utils.d.ts.map +0 -1
  132. package/dist/types/src/deprecated/schema/index.d.ts +0 -3
  133. package/dist/types/src/deprecated/schema/index.d.ts.map +0 -1
  134. package/dist/types/src/deprecated/schema/types.d.ts +0 -27
  135. package/dist/types/src/deprecated/schema/types.d.ts.map +0 -1
  136. package/dist/types/src/deprecated/theme.d.ts +0 -29
  137. package/dist/types/src/deprecated/theme.d.ts.map +0 -1
  138. package/dist/types/src/deprecated/translations.d.ts +0 -21
  139. package/dist/types/src/deprecated/translations.d.ts.map +0 -1
  140. package/dist/types/src/deprecated/types.d.ts +0 -26
  141. package/dist/types/src/deprecated/types.d.ts.map +0 -1
  142. package/src/deprecated/components/Cell/Cell.tsx +0 -88
  143. package/src/deprecated/components/Cell/CellCombobox.tsx +0 -103
  144. package/src/deprecated/components/Cell/index.ts +0 -5
  145. package/src/deprecated/components/ColumnMenu/ColumnMenu.stories.tsx +0 -129
  146. package/src/deprecated/components/ColumnMenu/ColumnMenu.tsx +0 -182
  147. package/src/deprecated/components/ColumnMenu/ColumnSettings.stories.tsx +0 -108
  148. package/src/deprecated/components/ColumnMenu/ColumnSettings.tsx +0 -207
  149. package/src/deprecated/components/ColumnMenu/index.ts +0 -6
  150. package/src/deprecated/components/Table/Table.stories.tsx +0 -508
  151. package/src/deprecated/components/Table/Table.tsx +0 -301
  152. package/src/deprecated/components/Table/TableBody.tsx +0 -82
  153. package/src/deprecated/components/Table/TableContext.tsx +0 -34
  154. package/src/deprecated/components/Table/TableHead.tsx +0 -78
  155. package/src/deprecated/components/Table/TableRootContext.ts +0 -39
  156. package/src/deprecated/components/Table/index.ts +0 -8
  157. package/src/deprecated/components/Table/props.ts +0 -60
  158. package/src/deprecated/components/index.ts +0 -7
  159. package/src/deprecated/helpers.tsx +0 -526
  160. package/src/deprecated/hooks/index.ts +0 -8
  161. package/src/deprecated/hooks/useColumnSizing.ts +0 -46
  162. package/src/deprecated/hooks/useColumnSorting.ts +0 -64
  163. package/src/deprecated/hooks/usePinLastRow.ts +0 -47
  164. package/src/deprecated/hooks/useRowSelection.ts +0 -40
  165. package/src/deprecated/index.ts +0 -12
  166. package/src/deprecated/schema/Schema.stories.tsx +0 -59
  167. package/src/deprecated/schema/column-utils.ts +0 -54
  168. package/src/deprecated/schema/index.ts +0 -6
  169. package/src/deprecated/schema/types.ts +0 -40
  170. package/src/deprecated/theme.ts +0 -82
  171. package/src/deprecated/translations.ts +0 -26
  172. package/src/deprecated/types.ts +0 -38
@@ -8,6 +8,8 @@ import React, { useCallback, useMemo, useRef } from 'react';
8
8
  import { FormatEnum } from '@dxos/echo-schema';
9
9
  import { invariant } from '@dxos/invariant';
10
10
  import { type DxGrid } from '@dxos/lit-grid';
11
+ import { useThemeContext } from '@dxos/react-ui';
12
+ import { createMarkdownExtensions } from '@dxos/react-ui-editor';
11
13
  import {
12
14
  cellQuery,
13
15
  editorKeys,
@@ -21,6 +23,7 @@ import {
21
23
  type GridCellEditorProps,
22
24
  type GridScopedProps,
23
25
  } from '@dxos/react-ui-grid';
26
+ import { tagPickerExtension, createLinks } from '@dxos/react-ui-tag-picker';
24
27
  import { type FieldProjection } from '@dxos/schema';
25
28
 
26
29
  import { completion } from './extension';
@@ -55,6 +58,7 @@ export const TableCellEditor = ({
55
58
  }: GridScopedProps<TableCellEditorProps>) => {
56
59
  const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
57
60
  const suppressNextBlur = useRef(false);
61
+ const { themeMode } = useThemeContext();
58
62
 
59
63
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
60
64
  if (!model || !editing) {
@@ -80,7 +84,7 @@ export const TableCellEditor = ({
80
84
  onFocus?.();
81
85
  setEditing(null);
82
86
  },
83
- [model, editing],
87
+ [model, editing, onEnter, onFocus, setEditing],
84
88
  );
85
89
 
86
90
  const handleBlur = useCallback<EditorBlurHandler>(
@@ -113,7 +117,7 @@ export const TableCellEditor = ({
113
117
  onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
114
118
  }
115
119
  },
116
- [model, editing, onFocus, onEnter, fieldProjection, determineNavigationAxis, determineNavigationDelta],
120
+ [model, editing, onFocus, onEnter, fieldProjection],
117
121
  );
118
122
 
119
123
  const extension = useMemo(() => {
@@ -128,6 +132,50 @@ export const TableCellEditor = ({
128
132
  }),
129
133
  ];
130
134
 
135
+ const format = fieldProjection.props.format;
136
+
137
+ if (format === FormatEnum.SingleSelect || format === FormatEnum.MultiSelect) {
138
+ // TODO(ZaymonFC): Reconcile this with the TagPicker component?
139
+ // Add markdown extensions needed by tag picker.
140
+ extension.push(createMarkdownExtensions({ themeMode }));
141
+
142
+ const options = fieldProjection.props.options || [];
143
+
144
+ const mode = format === FormatEnum.SingleSelect ? ('single-select' as const) : ('multi-select' as const);
145
+
146
+ extension.push(
147
+ tagPickerExtension({
148
+ mode,
149
+ inGrid: true,
150
+ onSearch: (text, selectedIds) => {
151
+ return options
152
+ .filter(
153
+ (option) =>
154
+ selectedIds.indexOf(option.id) === -1 &&
155
+ (text.length === 0 || option.title.toLowerCase().includes(text.toLowerCase())),
156
+ )
157
+ .map((option) => ({
158
+ id: option.id,
159
+ label: option.title,
160
+ hue: option.color as any,
161
+ }));
162
+ },
163
+ onUpdate: (ids) => {
164
+ if (model && editing) {
165
+ if (ids.length === 0) {
166
+ return;
167
+ }
168
+ if (mode === 'single-select') {
169
+ handleEnter(ids[0]);
170
+ } else {
171
+ handleEnter(ids);
172
+ }
173
+ }
174
+ },
175
+ }),
176
+ );
177
+ }
178
+
131
179
  if (onQuery) {
132
180
  switch (fieldProjection.props.format) {
133
181
  case FormatEnum.Ref: {
@@ -164,11 +212,61 @@ export const TableCellEditor = ({
164
212
  }
165
213
 
166
214
  return extension;
167
- }, [model, modals, editing, fieldProjection, handleClose]);
215
+ }, [model, modals, editing, fieldProjection, handleClose, themeMode]);
168
216
 
169
217
  const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
170
218
  if (model && editing) {
171
- const value = model.getCellData(parseCellIndex(editing.index));
219
+ const cell = parseCellIndex(editing.index);
220
+ const { col } = cell;
221
+ const field = model.projection.view.fields[col];
222
+ const fieldProjection = model.projection.getFieldProjection(field.id);
223
+
224
+ if (
225
+ fieldProjection?.props.format === FormatEnum.SingleSelect ||
226
+ fieldProjection?.props.format === FormatEnum.MultiSelect
227
+ ) {
228
+ const value = model.getCellData(cell);
229
+
230
+ if (value !== undefined) {
231
+ const options = fieldProjection.props.options || [];
232
+
233
+ if (fieldProjection.props.format === FormatEnum.MultiSelect && Array.isArray(value)) {
234
+ const tagItems = value
235
+ .map((id) => {
236
+ const option = options.find((o) => o.id === id);
237
+ if (option) {
238
+ return {
239
+ id,
240
+ label: option.title,
241
+ hue: option.color as any,
242
+ };
243
+ }
244
+ return undefined;
245
+ })
246
+ .filter((item): item is { id: any; label: string; hue: any } => item !== undefined);
247
+
248
+ return createLinks(tagItems);
249
+ } else {
250
+ const option = options.find((o) => o.id === value);
251
+
252
+ if (option) {
253
+ const tagItem = {
254
+ id: value,
255
+ label: option.title,
256
+ hue: option.color as any,
257
+ };
258
+
259
+ return createLinks([tagItem]);
260
+ }
261
+ }
262
+ }
263
+
264
+ // Return empty string if no value
265
+ return '';
266
+ }
267
+
268
+ // Default behavior for other formats
269
+ const value = model.getCellData(cell);
172
270
  return value !== undefined ? String(value) : '';
173
271
  }
174
272
  }, [model, editing]);
@@ -8,7 +8,6 @@ import { setValue, toJsonSchema, S, TypeEnum, TypedObject, FormatEnum } from '@d
8
8
  import { faker } from '@dxos/random';
9
9
  import { create, makeRef } from '@dxos/react-client/echo';
10
10
  import { createView, type ViewProjection } from '@dxos/schema';
11
- import {} from '@dxos/schema';
12
11
 
13
12
  import { TableType } from '../types';
14
13
 
@@ -10,28 +10,28 @@ import { useSelectionActions } from '@dxos/react-ui-attention';
10
10
  import { type ViewProjection } from '@dxos/schema';
11
11
  import { isNonNullable } from '@dxos/util';
12
12
 
13
- import { type BaseTableRow, TableModel, type TableModelProps, type TableRowAction } from '../model';
13
+ import { type TableRow, TableModel, type TableModelProps, type TableRowAction } from '../model';
14
14
  import { type TableType } from '../types';
15
15
 
16
- export type UseTableModelParams<T extends BaseTableRow = { id: string }> = {
16
+ export type UseTableModelParams<T extends TableRow = TableRow> = {
17
17
  table?: TableType;
18
18
  projection?: ViewProjection;
19
- objects?: ReactiveObject<T>[];
20
- onSelectionChanged?: (selection: string[]) => void;
19
+ rows?: ReactiveObject<T>[];
21
20
  rowActions?: TableRowAction[];
21
+ onSelectionChanged?: (selection: string[]) => void;
22
22
  onRowAction?: (actionId: string, data: T) => void;
23
23
  } & Pick<
24
24
  TableModelProps<T>,
25
- 'features' | 'onInsertRow' | 'onDeleteRows' | 'onDeleteColumn' | 'onCellUpdate' | 'onRowOrderChanged'
25
+ 'features' | 'onInsertRow' | 'onDeleteRows' | 'onDeleteColumn' | 'onCellUpdate' | 'onRowOrderChange'
26
26
  >;
27
27
 
28
- export const useTableModel = <T extends BaseTableRow = { id: string }>({
29
- objects,
28
+ export const useTableModel = <T extends TableRow = TableRow>({
30
29
  table,
31
30
  projection,
31
+ rows,
32
+ rowActions,
32
33
  features,
33
34
  onSelectionChanged,
34
- rowActions,
35
35
  onRowAction,
36
36
  ...props
37
37
  }: UseTableModelParams<T>): TableModel<T> | undefined => {
@@ -65,10 +65,10 @@ export const useTableModel = <T extends BaseTableRow = { id: string }>({
65
65
 
66
66
  // Update data.
67
67
  useEffect(() => {
68
- if (objects) {
69
- model?.setRows(objects);
68
+ if (rows) {
69
+ model?.setRows(rows);
70
70
  }
71
- }, [model, objects]);
71
+ }, [model, rows]);
72
72
 
73
73
  const { select, clear } = useSelectionActions([table?.id, table?.view?.target?.query.typename].filter(isNonNullable));
74
74
 
@@ -5,3 +5,4 @@
5
5
  export * from './table-model';
6
6
  export * from './modal-controller';
7
7
  export * from './table-presentation';
8
+ export type { SelectionMode } from './selection-model';
@@ -6,10 +6,12 @@ import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-c
6
6
 
7
7
  import { Resource } from '@dxos/context';
8
8
 
9
- import { type BaseTableRow } from './table-model';
9
+ import { type TableRow } from './table-model';
10
10
  import { touch } from '../util';
11
11
 
12
- export class SelectionModel<T extends BaseTableRow> extends Resource {
12
+ export type SelectionMode = 'single' | 'multiple';
13
+
14
+ export class SelectionModel<T extends TableRow> extends Resource {
13
15
  private readonly _selection = signal<Set<string>>(new Set());
14
16
 
15
17
  private readonly _validSelectedIds = computed<Set<string>>(() => {
@@ -35,6 +37,7 @@ export class SelectionModel<T extends BaseTableRow> extends Resource {
35
37
 
36
38
  constructor(
37
39
  private readonly _rows: ReadonlySignal<T[]>,
40
+ private readonly _selectionMode: SelectionMode,
38
41
  private readonly _onSelectionChanged?: () => void,
39
42
  ) {
40
43
  super();
@@ -64,6 +67,10 @@ export class SelectionModel<T extends BaseTableRow> extends Resource {
64
67
  return this._allSelected;
65
68
  }
66
69
 
70
+ public get selectionMode(): SelectionMode {
71
+ return this._selectionMode;
72
+ }
73
+
67
74
  public getSelectedRows = (): T[] => {
68
75
  const selectedIds = this._validSelectedIds.value;
69
76
  return this._rows.value.filter((row) => selectedIds.has(row.id));
@@ -89,6 +96,9 @@ export class SelectionModel<T extends BaseTableRow> extends Resource {
89
96
  if (newSelection.has(row.id)) {
90
97
  newSelection.delete(row.id);
91
98
  } else {
99
+ if (this._selectionMode === 'single') {
100
+ newSelection.clear();
101
+ }
92
102
  newSelection.add(row.id);
93
103
  }
94
104
 
@@ -106,7 +106,7 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
106
106
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
107
107
  const schema = createEchoSchema(Test);
108
108
  const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
109
- const projection = new ViewProjection(schema, view);
109
+ const projection = new ViewProjection(schema.jsonSchema, view);
110
110
  const table = create(TableType, { view: makeRef(view) });
111
111
  return new TableModel({ id: table.id, space: undefined, view, projection, ...props });
112
112
  };
@@ -18,31 +18,31 @@ import {
18
18
  } from '@dxos/react-ui-grid';
19
19
  import { type ViewType, type ViewProjection } from '@dxos/schema';
20
20
 
21
- import { SelectionModel } from './selection-model';
21
+ import { type SelectionMode, SelectionModel } from './selection-model';
22
22
  import { TableSorting } from './table-sorting';
23
23
  import { touch } from '../util';
24
24
 
25
- export type BaseTableRow = Record<JsonProp, any> & { id: string };
25
+ // TODO(burdon): Use schema types.
26
+ export type TableRow = Record<JsonProp, any> & { id: string };
26
27
 
27
28
  export type TableRowAction = {
28
29
  id: string;
29
30
  translationKey: string;
30
31
  };
31
32
 
32
- // TODO(ZaymonFC): There should be a separate concept for immutable schemata.
33
33
  export type TableFeatures = {
34
- selection: boolean;
34
+ selection: { enabled: boolean; mode?: SelectionMode };
35
35
  dataEditable: boolean;
36
36
  schemaEditable: boolean;
37
37
  };
38
38
 
39
39
  const defaultFeatures: TableFeatures = {
40
- selection: true,
40
+ selection: { enabled: true, mode: 'multiple' },
41
41
  dataEditable: false,
42
42
  schemaEditable: false,
43
43
  };
44
44
 
45
- export type TableModelProps<T extends BaseTableRow = { id: string }> = {
45
+ export type TableModelProps<T extends TableRow = TableRow> = {
46
46
  id?: string;
47
47
  space?: Space;
48
48
  view?: ViewType;
@@ -50,16 +50,16 @@ export type TableModelProps<T extends BaseTableRow = { id: string }> = {
50
50
  features?: Partial<TableFeatures>;
51
51
  sorting?: FieldSortType[];
52
52
  pinnedRows?: { top: number[]; bottom: number[] };
53
+ rowActions?: TableRowAction[];
53
54
  onInsertRow?: (index?: number) => void;
54
55
  onDeleteRows?: (index: number, obj: T[]) => void;
55
56
  onDeleteColumn?: (fieldId: string) => void;
56
57
  onCellUpdate?: (cell: DxGridPosition) => void;
57
- onRowOrderChanged?: () => void;
58
- rowActions?: TableRowAction[];
58
+ onRowOrderChange?: () => void;
59
59
  onRowAction?: (actionId: string, data: T) => void;
60
60
  };
61
61
 
62
- export class TableModel<T extends BaseTableRow = { id: string }> extends Resource {
62
+ export class TableModel<T extends TableRow = TableRow> extends Resource {
63
63
  private readonly _id: string | undefined;
64
64
  private readonly _space: Space | undefined;
65
65
  private readonly _view: ViewType | undefined;
@@ -74,9 +74,9 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
74
74
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
75
75
  private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
76
76
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
77
- private readonly _onRowOrderChanged?: TableModelProps<T>['onRowOrderChanged'];
78
- private readonly _rowActions: TableRowAction[];
77
+ private readonly _onRowOrderChange?: TableModelProps<T>['onRowOrderChange'];
79
78
  private readonly _onRowAction?: TableModelProps<T>['onRowAction'];
79
+ private readonly _rowActions: TableRowAction[];
80
80
  private readonly _features: TableFeatures;
81
81
 
82
82
  private readonly _rows = signal<T[]>([]);
@@ -94,12 +94,12 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
94
94
  features = {},
95
95
  sorting = [],
96
96
  pinnedRows = { top: [], bottom: [] },
97
+ rowActions = [],
97
98
  onCellUpdate,
98
99
  onDeleteColumn,
99
100
  onDeleteRows,
100
101
  onInsertRow,
101
- onRowOrderChanged,
102
- rowActions = [],
102
+ onRowOrderChange,
103
103
  onRowAction,
104
104
  }: TableModelProps<T>) {
105
105
  super();
@@ -107,8 +107,15 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
107
107
  this._space = space;
108
108
  this._view = view;
109
109
  this._projection = projection;
110
+
111
+ // TODO(ZaymonFC): Use our more robust config merging module?
110
112
  this._features = { ...defaultFeatures, ...features };
111
113
 
114
+ invariant(
115
+ !(this._features.dataEditable && this._features.selection.enabled && this._features.selection.mode === 'single'),
116
+ 'Single selection is not compatible with editable tables.',
117
+ );
118
+
112
119
  this._sorting = new TableSorting(this._rows, this._view, projection);
113
120
 
114
121
  if (sorting.length > 0) {
@@ -117,12 +124,12 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
117
124
  }
118
125
 
119
126
  this._pinnedRows = pinnedRows;
127
+ this._rowActions = rowActions;
120
128
  this._onInsertRow = onInsertRow;
121
129
  this._onDeleteRows = onDeleteRows;
122
130
  this._onDeleteColumn = onDeleteColumn;
123
131
  this._onCellUpdate = onCellUpdate;
124
- this._onRowOrderChanged = onRowOrderChanged;
125
- this._rowActions = rowActions;
132
+ this._onRowOrderChange = onRowOrderChange;
126
133
  this._onRowAction = onRowAction;
127
134
  }
128
135
 
@@ -197,7 +204,9 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
197
204
  protected override async _open() {
198
205
  this.initializeColumnMeta();
199
206
  this.initializeEffects();
200
- this._selection = new SelectionModel(this._sorting.sortedRows, () => this._onRowOrderChanged?.());
207
+ this._selection = new SelectionModel(this._sorting.sortedRows, this._features.selection.mode ?? 'multiple', () =>
208
+ this._onRowOrderChange?.(),
209
+ );
201
210
  await this._selection.open(this._ctx);
202
211
  }
203
212
 
@@ -219,7 +228,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
219
228
  private initializeEffects(): void {
220
229
  const rowOrderWatcher = effect(() => {
221
230
  touch(this._sorting.sortedRows.value);
222
- this._onRowOrderChanged?.();
231
+ this._onRowOrderChange?.();
223
232
  });
224
233
  this._ctx.onDispose(rowOrderWatcher);
225
234
 
@@ -268,7 +277,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
268
277
  const obj = this._rows.value[row];
269
278
  const objectsToDelete = [];
270
279
 
271
- if (this._selection.hasSelection.value) {
280
+ if (this.features.selection.enabled && this._selection.hasSelection.value) {
272
281
  const selectedRows = this._selection.getSelectedRows();
273
282
  objectsToDelete.push(...selectedRows);
274
283
  } else {
@@ -96,7 +96,7 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
96
96
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
97
97
  const schema = createEchoSchema(Test);
98
98
  const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
99
- const projection = new ViewProjection(schema, view);
99
+ const projection = new ViewProjection(schema.jsonSchema, view);
100
100
  const table = create(TableType, { view: makeRef(view) });
101
101
  return new TableModel({ id: table.id, view, projection, ...props });
102
102
  };
@@ -5,18 +5,19 @@
5
5
  import { signal } from '@preact/signals-core';
6
6
 
7
7
  import { FormatEnum, getValue } from '@dxos/echo-schema';
8
- import { cellClassesForFieldType, cellClassesForRowSelection, formatForDisplay } from '@dxos/react-ui-form';
8
+ import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
9
9
  import {
10
10
  type DxGridPlane,
11
11
  type DxGridPlaneCells,
12
- toPlaneCellIndex,
13
12
  type DxGridPlaneRange,
14
13
  type DxGridCellValue,
14
+ toPlaneCellIndex,
15
15
  } from '@dxos/react-ui-grid';
16
16
  import { mx } from '@dxos/react-ui-theme';
17
17
  import { VIEW_FIELD_LIMIT, type FieldType } from '@dxos/schema';
18
18
 
19
- import { type BaseTableRow, type TableModel } from './table-model';
19
+ import { type SelectionMode } from './selection-model';
20
+ import { type TableRow, type TableModel } from './table-model';
20
21
  import { tableButtons, tableControls } from '../util';
21
22
 
22
23
  /**
@@ -24,7 +25,7 @@ import { tableButtons, tableControls } from '../util';
24
25
  * Manages rendering of table cells, headers, selection columns, and action columns across
25
26
  * different grid planes.
26
27
  */
27
- export class TablePresentation<T extends BaseTableRow = { id: string }> {
28
+ export class TablePresentation<T extends TableRow = TableRow> {
28
29
  constructor(
29
30
  private readonly model: TableModel<T>,
30
31
  private readonly _visibleRange = signal<DxGridPlaneRange>({
@@ -62,7 +63,7 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
62
63
 
63
64
  if (plane === 'grid' && this.model.features.dataEditable === false) {
64
65
  Object.values(cells).forEach((cell) => {
65
- cell.readonly = true;
66
+ cell.readonly = 'text-select';
66
67
  });
67
68
  }
68
69
 
@@ -85,7 +86,8 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
85
86
 
86
87
  switch (props.format) {
87
88
  case FormatEnum.Boolean:
88
- case FormatEnum.SingleSelect: {
89
+ case FormatEnum.SingleSelect:
90
+ case FormatEnum.MultiSelect: {
89
91
  return '';
90
92
  }
91
93
  case FormatEnum.Ref: {
@@ -108,7 +110,10 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
108
110
  if (formatClasses) {
109
111
  classes.push(formatClasses);
110
112
  }
111
- const rowSelectionClasses = cellClassesForRowSelection(this.model.selection.isObjectSelected(obj));
113
+ const rowSelectionClasses = cellClassesForRowSelection(
114
+ this.model.selection.isObjectSelected(obj),
115
+ this.model.selection.selectionMode,
116
+ );
112
117
  if (rowSelectionClasses) {
113
118
  classes.push(rowSelectionClasses);
114
119
  }
@@ -133,7 +138,7 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
133
138
  rowIndex: displayIndex,
134
139
  checked: value ?? false,
135
140
  });
136
- cell.readonly = true;
141
+ cell.readonly = 'no-text-select';
137
142
  }
138
143
 
139
144
  if (props.format === FormatEnum.SingleSelect) {
@@ -147,6 +152,27 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
147
152
  }
148
153
  }
149
154
 
155
+ if (props.format === FormatEnum.MultiSelect) {
156
+ const values = getValue(obj, field.path) as string[] | undefined;
157
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
158
+ if (options && values && values.length > 0) {
159
+ const tags = values
160
+ .map((value) => {
161
+ const option = options.find((o) => o.id === value);
162
+ if (option) {
163
+ return `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
164
+ }
165
+ return null;
166
+ })
167
+ .filter(Boolean)
168
+ .join(' ');
169
+
170
+ if (tags) {
171
+ cell.accessoryHtml = tags;
172
+ }
173
+ }
174
+ }
175
+
150
176
  const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
151
177
  cells[idx] = cell;
152
178
  };
@@ -193,10 +219,10 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
193
219
  const cells: DxGridPlaneCells = {};
194
220
  for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
195
221
  const isSelected = this.model.selection.isRowIndexSelected(row);
196
- const classes = cellClassesForRowSelection(isSelected);
222
+ const classes = cellClassesForRowSelection(isSelected, this.model.selection.selectionMode);
197
223
  cells[toPlaneCellIndex({ col: 0, row })] = {
198
224
  value: '',
199
- readonly: true,
225
+ readonly: 'no-text-select',
200
226
  className: classes ? mx(classes) : undefined,
201
227
  accessoryHtml: tableControls.checkbox.render({ rowIndex: row, checked: isSelected }),
202
228
  };
@@ -209,7 +235,7 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
209
235
  const cells: DxGridPlaneCells = {};
210
236
  for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
211
237
  const isSelected = this.model.selection.isRowIndexSelected(row);
212
- const classes = cellClassesForRowSelection(isSelected);
238
+ const classes = cellClassesForRowSelection(isSelected, this.model.selection.selectionMode);
213
239
  cells[toPlaneCellIndex({ col: 0, row })] = {
214
240
  value: '',
215
241
  readonly: true,
@@ -222,6 +248,16 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
222
248
  }
223
249
 
224
250
  private getSelectAllCell(): DxGridPlaneCells {
251
+ if (!this.model.features.selection.enabled || this.model.selection.selectionMode === 'single') {
252
+ return {
253
+ [toPlaneCellIndex({ col: 0, row: 0 })]: {
254
+ className: '!bg-gridHeader',
255
+ readonly: true,
256
+ value: '',
257
+ },
258
+ };
259
+ }
260
+
225
261
  return {
226
262
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
227
263
  accessoryHtml: tableControls.checkbox.render({
@@ -251,3 +287,21 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
251
287
  };
252
288
  }
253
289
  }
290
+
291
+ export const cellClassesForRowSelection = (selected: boolean, selectionMode: SelectionMode) => {
292
+ if (!selected) {
293
+ if (selectionMode === 'single') {
294
+ return ['!cursor-pointer'];
295
+ } else {
296
+ return undefined;
297
+ }
298
+ }
299
+
300
+ switch (selectionMode) {
301
+ case 'single':
302
+ // TODO(ZaymonFC): @thure, do we need a grid version of 'currentRelated'?
303
+ return ['!bg-currentRelated hover:bg-hoverSurface', '!cursor-pointer'];
304
+ case 'multiple':
305
+ return ['!bg-gridCellSelected'];
306
+ }
307
+ };
@@ -9,7 +9,7 @@ import { getValue, FormatEnum, TypeEnum, type SortDirectionType, type FieldSortT
9
9
  import { formatForDisplay } from '@dxos/react-ui-form';
10
10
  import type { PropertyType, FieldType, ViewProjection, ViewType } from '@dxos/schema';
11
11
 
12
- import { type BaseTableRow } from './table-model';
12
+ import { type TableRow } from './table-model';
13
13
 
14
14
  /**
15
15
  * Represents the local sort state.
@@ -40,7 +40,7 @@ type LocalSort = { type: 'cleared' } | { type: 'active'; sort: FieldSortType } |
40
40
  * │ 2: "Cherry" │ => │ 2: "Cherry" │ => │ 2 => 2 │
41
41
  * └──────────────┘ └──────────────┘ └────────┘
42
42
  */
43
- export class TableSorting<T extends BaseTableRow> {
43
+ export class TableSorting<T extends TableRow> {
44
44
  private readonly _displayToDataIndex = new Map<number, number>();
45
45
  private readonly _rows: Signal<T[]>;
46
46
  private readonly _localSort = signal<LocalSort>(undefined);
@@ -4,12 +4,11 @@
4
4
 
5
5
  import { expect, test } from '@playwright/test';
6
6
 
7
- import { setupPage } from '@dxos/test-utils/playwright';
7
+ import { setupPage, storybookUrl } from '@dxos/test-utils/playwright';
8
8
 
9
9
  import { TableManager } from './TableManager';
10
10
 
11
- const storyId = 'ui-react-ui-table-table';
12
- const storyUrl = `http://localhost:9009/iframe.html?id=${storyId}&viewMode=story`;
11
+ const storyUrl = storybookUrl('ui-react-ui-table-table--default');
13
12
 
14
13
  // NOTE(ZaymonFC): This test suite relies on the faker seed being set to 0 in the story.
15
14
  test.describe('Table', () => {
@@ -7,14 +7,15 @@ import { EchoObject, Expando, LabelAnnotationId, ObjectId, Ref, S } from '@dxos/
7
7
  import { ViewType } from '@dxos/schema';
8
8
 
9
9
  export const TableSchema = S.Struct({
10
- id: ObjectId, // TODO(burdon): Where should this be?
10
+ id: ObjectId,
11
11
  name: S.optional(S.String),
12
12
  view: S.optional(Ref(ViewType)),
13
- // TODO(burdon): Should not import from plugin. Either factor out type or use reverse deps when supported.
14
- threads: S.optional(S.Array(Ref(Expando /* ThreadType */))), // TODO(dmaretskyi): Breaks edge because plugin-space depends on react-client.
13
+ // TODO(burdon): Document why threads is included here?
14
+ threads: S.optional(S.Array(Ref(Expando /* ThreadType */))),
15
15
  }).annotations({
16
+ // TODO(burdon): Move annotation to property.
16
17
  [LabelAnnotationId]: 'name',
17
18
  });
18
19
 
19
- export const TableType = TableSchema.pipe(EchoObject('dxos.org/type/Table', '0.1.0'));
20
- export type TableType = S.Schema.Type<typeof TableType>;
20
+ export const TableType = TableSchema.pipe(EchoObject({ typename: 'dxos.org/type/Table', version: '0.1.0' }));
21
+ export interface TableType extends S.Schema.Type<typeof TableType> {}