@dxos/react-ui-table 0.7.4 → 0.7.5-main.937ce75

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 (93) hide show
  1. package/dist/lib/browser/{chunk-IYCSJDMB.mjs → chunk-AT7O4RAD.mjs} +4 -4
  2. package/dist/lib/browser/{chunk-IYCSJDMB.mjs.map → chunk-AT7O4RAD.mjs.map} +2 -2
  3. package/dist/lib/browser/index.mjs +1442 -1219
  4. package/dist/lib/browser/index.mjs.map +4 -4
  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-MHBMVCFZ.cjs → chunk-YSBXNBDJ.cjs} +6 -6
  8. package/dist/lib/node/{chunk-MHBMVCFZ.cjs.map → chunk-YSBXNBDJ.cjs.map} +2 -2
  9. package/dist/lib/node/index.cjs +1351 -1111
  10. package/dist/lib/node/index.cjs.map +4 -4
  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-BMWGPXPL.mjs → chunk-6OH2NHUX.mjs} +4 -4
  15. package/dist/lib/node-esm/{chunk-BMWGPXPL.mjs.map → chunk-6OH2NHUX.mjs.map} +2 -2
  16. package/dist/lib/node-esm/index.mjs +1442 -1219
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  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/_deprecated/hooks/useColumnSorting.d.ts +1 -1
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +4 -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 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/CreateRefPanel.d.ts +3 -2
  26. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/RefPanel.d.ts +4 -3
  28. package/dist/types/src/components/Table/RefPanel.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/RowActionsMenu.d.ts +3 -2
  30. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/Table.d.ts +2 -1
  32. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  34. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +3 -2
  35. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  36. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/testing.d.ts +55 -71
  38. package/dist/types/src/components/testing.d.ts.map +1 -1
  39. package/dist/types/src/index.d.ts +1 -0
  40. package/dist/types/src/index.d.ts.map +1 -1
  41. package/dist/types/src/model/index.d.ts +1 -0
  42. package/dist/types/src/model/index.d.ts.map +1 -1
  43. package/dist/types/src/model/modal-controller.d.ts +6 -2
  44. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  45. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  46. package/dist/types/src/model/table-model.d.ts +14 -27
  47. package/dist/types/src/model/table-model.d.ts.map +1 -1
  48. package/dist/types/src/model/table-presentation.d.ts +22 -0
  49. package/dist/types/src/model/table-presentation.d.ts.map +1 -0
  50. package/dist/types/src/model/table-presentation.test.d.ts +2 -0
  51. package/dist/types/src/model/table-presentation.test.d.ts.map +1 -0
  52. package/dist/types/src/model/table-sorting.d.ts +48 -0
  53. package/dist/types/src/model/table-sorting.d.ts.map +1 -0
  54. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  55. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  56. package/dist/types/src/types/table.d.ts +118 -154
  57. package/dist/types/src/types/table.d.ts.map +1 -1
  58. package/dist/types/src/util/index.d.ts +1 -0
  59. package/dist/types/src/util/index.d.ts.map +1 -1
  60. package/dist/types/src/util/table-buttons.d.ts +50 -20
  61. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  62. package/dist/types/src/util/table-controls.d.ts +32 -9
  63. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  64. package/dist/types/src/util/table.d.ts +2 -2
  65. package/dist/types/src/util/table.d.ts.map +1 -1
  66. package/dist/types/tsconfig.tsbuildinfo +1 -0
  67. package/package.json +29 -29
  68. package/src/components/Table/ColumnActionsMenu.tsx +8 -8
  69. package/src/components/Table/ColumnSettings.tsx +11 -11
  70. package/src/components/Table/CreateRefPanel.tsx +13 -14
  71. package/src/components/Table/RefPanel.tsx +9 -9
  72. package/src/components/Table/RowActionsMenu.tsx +6 -7
  73. package/src/components/Table/Table.stories.tsx +40 -23
  74. package/src/components/Table/Table.tsx +235 -148
  75. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +13 -12
  76. package/src/components/TableCellEditor/TableCellEditor.tsx +6 -4
  77. package/src/components/testing.ts +10 -8
  78. package/src/index.ts +2 -0
  79. package/src/model/index.ts +1 -0
  80. package/src/model/modal-controller.ts +15 -48
  81. package/src/model/selection-model.ts +12 -1
  82. package/src/model/table-model.test.ts +4 -71
  83. package/src/model/table-model.ts +61 -292
  84. package/src/model/table-presentation.test.ts +102 -0
  85. package/src/model/table-presentation.ts +217 -0
  86. package/src/model/table-sorting.ts +131 -0
  87. package/src/playwright/TableManager.ts +5 -1
  88. package/src/playwright/smoke.spec.ts +79 -1
  89. package/src/types/table.ts +3 -3
  90. package/src/util/index.ts +1 -0
  91. package/src/util/table-buttons.ts +97 -48
  92. package/src/util/table-controls.ts +71 -7
  93. package/src/util/table.ts +21 -13
@@ -3,40 +3,25 @@
3
3
  //
4
4
 
5
5
  import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-core';
6
- import orderBy from 'lodash.orderby';
7
6
 
8
7
  import { Resource } from '@dxos/context';
9
8
  import { getValue, setValue, FormatEnum, type JsonProp } from '@dxos/echo-schema';
10
9
  import { invariant } from '@dxos/invariant';
10
+ import { isReactiveObject, makeRef } from '@dxos/live-object';
11
11
  import { PublicKey } from '@dxos/react-client';
12
- import {
13
- cellClassesForFieldType,
14
- cellClassesForRowSelection,
15
- formatForDisplay,
16
- formatForEditing,
17
- parseValue,
18
- } from '@dxos/react-ui-form';
12
+ import { formatForEditing, parseValue } from '@dxos/react-ui-form';
19
13
  import {
20
14
  type DxGridAxisMeta,
21
- type DxGridCellValue,
22
- type DxGridPlane,
23
- type DxGridPlaneCells,
24
15
  type DxGridPlaneRange,
25
16
  type DxGridPlanePosition,
26
- toPlaneCellIndex,
27
17
  type DxGridPosition,
28
18
  } from '@dxos/react-ui-grid';
29
- import { mx } from '@dxos/react-ui-theme';
30
- import { VIEW_FIELD_LIMIT, type ViewProjection, type FieldType } from '@dxos/schema';
19
+ import { type ViewProjection } from '@dxos/schema';
31
20
 
32
- import { ModalController } from './modal-controller';
33
21
  import { SelectionModel } from './selection-model';
22
+ import { type SortConfig, type SortDirection, TableSorting } from './table-sorting';
34
23
  import { type TableType } from '../types';
35
- import { tableButtons, touch } from '../util';
36
- import { tableControls } from '../util/table-controls';
37
-
38
- export type SortDirection = 'asc' | 'desc';
39
- export type SortConfig = { fieldId: string; direction: SortDirection };
24
+ import { touch } from '../util';
40
25
 
41
26
  export type BaseTableRow = Record<JsonProp, any> & { id: string };
42
27
 
@@ -63,28 +48,18 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
63
48
  end: { row: 0, col: 0 },
64
49
  });
65
50
 
66
- /**
67
- * Maps display indices to data indices.
68
- * Used for translating between sorted/displayed order and original data order.
69
- * Keys are display indices, values are corresponding data indices.
70
- */
71
- private readonly _displayToDataIndex: Map<number, number> = new Map();
72
-
73
51
  private readonly _onInsertRow?: TableModelProps<T>['onInsertRow'];
74
52
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
75
53
  private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
76
54
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
77
55
  private readonly _onRowOrderChanged?: TableModelProps<T>['onRowOrderChanged'];
78
56
 
79
- private readonly _sorting = signal<SortConfig | undefined>(undefined);
80
- private _sortedRows!: ReadonlySignal<T[]>;
81
- private _rows = signal<T[]>([]);
57
+ private readonly _rows = signal<T[]>([]);
58
+ private readonly _sorting: TableSorting<T>;
82
59
 
83
60
  private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
84
-
85
61
  private _selection!: SelectionModel<T>;
86
62
  private _columnMeta?: ReadonlySignal<DxGridAxisMeta>;
87
- private readonly _modalController = new ModalController();
88
63
 
89
64
  constructor({
90
65
  table,
@@ -100,8 +75,13 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
100
75
  super();
101
76
  this._table = table;
102
77
  this._projection = projection;
78
+ this._sorting = new TableSorting(this._rows, table, projection);
79
+
80
+ if (sorting.length > 0) {
81
+ const [sort] = sorting;
82
+ this._sorting.setSort(sort.fieldId, sort.direction);
83
+ }
103
84
 
104
- this._sorting.value = sorting.at(0);
105
85
  this._pinnedRows = pinnedRows;
106
86
  this._onInsertRow = onInsertRow;
107
87
  this._onDeleteRows = onDeleteRows;
@@ -118,6 +98,17 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
118
98
  return this._projection;
119
99
  }
120
100
 
101
+ public get rows(): ReadonlySignal<T[]> {
102
+ return this._sorting.sortedRows;
103
+ }
104
+
105
+ /**
106
+ * @reactive
107
+ */
108
+ public get sorting(): SortConfig | undefined {
109
+ return this._sorting.sorting;
110
+ }
111
+
121
112
  public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
122
113
  return this._pinnedRows;
123
114
  }
@@ -127,14 +118,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
127
118
  return this._columnMeta;
128
119
  }
129
120
 
130
- public get sorting(): SortConfig | undefined {
131
- return this._sorting.value;
132
- }
133
-
134
- public get modalController() {
135
- return this._modalController;
136
- }
137
-
138
121
  public get selection() {
139
122
  return this._selection;
140
123
  }
@@ -145,15 +128,14 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
145
128
 
146
129
  protected override async _open() {
147
130
  this.initializeColumnMeta();
148
- this.initializeSorting();
149
131
  this.initializeEffects();
150
- this._selection = new SelectionModel(this._sortedRows, () => this._onRowOrderChanged?.());
132
+ this._selection = new SelectionModel(this._sorting.sortedRows, () => this._onRowOrderChanged?.());
151
133
  await this._selection.open(this._ctx);
152
134
  }
153
135
 
154
136
  private initializeColumnMeta(): void {
155
137
  this._columnMeta = computed(() => {
156
- const fields = this._table.view?.fields ?? [];
138
+ const fields = this._table.view?.target?.fields ?? [];
157
139
  const meta = Object.fromEntries(
158
140
  fields.map((field, index: number) => [index, { size: field?.size ?? 256, resizeable: true }]),
159
141
  );
@@ -166,39 +148,9 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
166
148
  });
167
149
  }
168
150
 
169
- private initializeSorting(): void {
170
- this._sortedRows = computed(() => {
171
- this._displayToDataIndex.clear();
172
- const sort = this._sorting.value;
173
- if (!sort) {
174
- return this._rows.value;
175
- }
176
-
177
- const field = this._table.view?.fields.find((field) => field.id === sort.fieldId);
178
- if (!field) {
179
- return this._rows.value;
180
- }
181
-
182
- const dataWithIndices = this._rows.value.map((item, index) => {
183
- const value = getValue(item, field.path);
184
- return { item, index, value, isEmpty: value == null || value === '' };
185
- });
186
- const sorted = orderBy(dataWithIndices, ['isEmpty', 'value'], ['asc', sort.direction]);
187
-
188
- for (let displayIndex = 0; displayIndex < sorted.length; displayIndex++) {
189
- const { index: dataIndex } = sorted[displayIndex];
190
- if (displayIndex !== dataIndex) {
191
- this._displayToDataIndex.set(displayIndex, dataIndex);
192
- }
193
- }
194
-
195
- return sorted.map(({ item }) => item);
196
- });
197
- }
198
-
199
151
  private initializeEffects(): void {
200
152
  const rowOrderWatcher = effect(() => {
201
- touch(this._sortedRows.value);
153
+ touch(this._sorting.sortedRows.value);
202
154
  this._onRowOrderChanged?.();
203
155
  });
204
156
  this._ctx.onDispose(rowOrderWatcher);
@@ -214,8 +166,8 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
214
166
  for (let row = start.row; row <= end.row; row++) {
215
167
  rowEffects.push(
216
168
  effect(() => {
217
- const obj = this._sortedRows.value[row];
218
- this?._table?.view?.fields.forEach((field) => touch(getValue(obj, field.path)));
169
+ const obj = this._sorting.sortedRows.value[row];
170
+ this?._table?.view?.target?.fields.forEach((field) => touch(getValue(obj, field.path)));
219
171
  this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
220
172
  }),
221
173
  );
@@ -226,181 +178,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
226
178
  this._ctx.onDispose(rowEffectManager);
227
179
  }
228
180
 
229
- //
230
- // Get Cells
231
- //
232
-
233
- public getCells = (range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells => {
234
- switch (plane) {
235
- case 'grid': {
236
- this._visibleRange.value = range;
237
- return this.getMainGridCells(range);
238
- }
239
- case 'frozenRowsStart': {
240
- return this.getHeaderCells(range);
241
- }
242
- case 'frozenColsStart': {
243
- return this.getSelectionColumnCells(range);
244
- }
245
- case 'frozenColsEnd': {
246
- return this.getActionColumnCells(range);
247
- }
248
- case 'fixedStartStart': {
249
- return this.getSelectAllCell();
250
- }
251
- case 'fixedStartEnd': {
252
- return this.getNewColumnCell();
253
- }
254
- default: {
255
- return {};
256
- }
257
- }
258
- };
259
-
260
- private getMainGridCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
261
- const cells: DxGridPlaneCells = {};
262
- const fields = this._table.view?.fields ?? [];
263
-
264
- const addCell = (obj: T, field: FieldType, colIndex: number, displayIndex: number): void => {
265
- const { props } = this._projection.getFieldProjection(field.id);
266
-
267
- const cell: DxGridCellValue = {
268
- get value() {
269
- const value = getValue(obj, field.path);
270
- if (value == null) {
271
- return '';
272
- }
273
-
274
- switch (props.format) {
275
- case FormatEnum.Ref: {
276
- if (!field.referencePath) {
277
- return ''; // TODO(burdon): Show error.
278
- }
279
-
280
- return getValue(value, field.referencePath);
281
- }
282
-
283
- default: {
284
- return formatForDisplay({ type: props.type, format: props.format, value });
285
- }
286
- }
287
- },
288
- };
289
-
290
- const classes = [];
291
- const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
292
- if (formatClasses) {
293
- classes.push(formatClasses);
294
- }
295
- const rowSelectionClasses = cellClassesForRowSelection(this._selection.isObjectSelected(obj));
296
- if (rowSelectionClasses) {
297
- classes.push(rowSelectionClasses);
298
- }
299
- if (classes.length > 0) {
300
- cell.className = mx(classes.flat());
301
- }
302
-
303
- if (cell.value && props.format === FormatEnum.Ref && props.referenceSchema) {
304
- const targetObj = getValue(obj, field.path);
305
- cell.accessoryHtml = tableButtons.referencedCell.render({
306
- targetId: targetObj.id,
307
- schemaId: props.referenceSchema,
308
- });
309
- }
310
-
311
- const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
312
- cells[idx] = cell;
313
- };
314
-
315
- for (let row = range.start.row; row <= range.end.row && row < this._sortedRows.value.length; row++) {
316
- for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
317
- const field = fields[col];
318
- if (!field) {
319
- continue;
320
- }
321
-
322
- addCell(this._sortedRows.value[row], field, col, row);
323
- }
324
- }
325
-
326
- return cells;
327
- };
328
-
329
- private getHeaderCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
330
- const cells: DxGridPlaneCells = {};
331
- const fields = this.table.view?.fields ?? [];
332
- for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
333
- const { field, props } = this._projection.getFieldProjection(fields[col].id);
334
- cells[toPlaneCellIndex({ col, row: 0 })] = {
335
- // TODO(burdon): Use same logic as form for fallback title.
336
- value: props.title ?? field.path,
337
- readonly: true,
338
- resizeHandle: 'col',
339
- accessoryHtml: tableButtons.columnSettings.render({ fieldId: field.id }),
340
- };
341
- }
342
-
343
- return cells;
344
- };
345
-
346
- private getSelectionColumnCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
347
- const cells: DxGridPlaneCells = {};
348
- for (let row = range.start.row; row <= range.end.row && row < this._rows.value.length; row++) {
349
- const isSelected = this._selection.isRowIndexSelected(row);
350
- const classes = cellClassesForRowSelection(isSelected);
351
- cells[toPlaneCellIndex({ col: 0, row })] = {
352
- value: '',
353
- readonly: true,
354
- className: classes ? mx(classes) : undefined,
355
- accessoryHtml: tableControls.checkbox.render({ rowIndex: row, checked: isSelected }),
356
- };
357
- }
358
-
359
- return cells;
360
- };
361
-
362
- private getActionColumnCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
363
- const cells: DxGridPlaneCells = {};
364
- for (let row = range.start.row; row <= range.end.row && row < this._rows.value.length; row++) {
365
- const isSelected = this._selection.isRowIndexSelected(row);
366
- const classes = cellClassesForRowSelection(isSelected);
367
- cells[toPlaneCellIndex({ col: 0, row })] = {
368
- value: '',
369
- readonly: true,
370
- className: classes ? mx(classes) : undefined,
371
- accessoryHtml: tableButtons.rowMenu.render({ rowIndex: row }),
372
- };
373
- }
374
-
375
- return cells;
376
- };
377
-
378
- private getSelectAllCell = (): DxGridPlaneCells => {
379
- return {
380
- [toPlaneCellIndex({ col: 0, row: 0 })]: {
381
- value: '',
382
- accessoryHtml: tableControls.checkbox.render({
383
- rowIndex: 0,
384
- header: true,
385
- checked: this._selection.allRowsSeleted.value,
386
- }),
387
- readonly: true,
388
- },
389
- };
390
- };
391
-
392
- private getNewColumnCell = (): DxGridPlaneCells => {
393
- return {
394
- [toPlaneCellIndex({ col: 0, row: 0 })]: {
395
- value: '',
396
- accessoryHtml: tableButtons.addColumn.render({
397
- disabled: (this._table.view?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
398
- }),
399
- readonly: true,
400
- },
401
- };
402
- };
403
-
404
181
  //
405
182
  // Data
406
183
  //
@@ -411,15 +188,15 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
411
188
 
412
189
  public getRowCount = (): number => this._rows.value.length;
413
190
 
414
- public getColumnCount = (): number => this.table.view?.fields.length ?? 0;
191
+ public getColumnCount = (): number => this.table.view?.target?.fields.length ?? 0;
415
192
 
416
193
  public insertRow = (rowIndex?: number): void => {
417
- const row = rowIndex !== undefined ? this._displayToDataIndex.get(rowIndex) ?? rowIndex : this._rows.value.length;
194
+ const row = rowIndex !== undefined ? this._sorting.getDataIndex(rowIndex) : this._rows.value.length;
418
195
  this._onInsertRow?.(row);
419
196
  };
420
197
 
421
198
  public deleteRow = (rowIndex: number): void => {
422
- const row = this._displayToDataIndex.get(rowIndex) ?? rowIndex;
199
+ const row = this._sorting.getDataIndex(rowIndex);
423
200
  const obj = this._rows.value[row];
424
201
  const objectsToDelete = [];
425
202
 
@@ -434,13 +211,13 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
434
211
  };
435
212
 
436
213
  public getCellData = ({ col, row }: DxGridPlanePosition): any => {
437
- const fields = this.table.view?.fields ?? [];
214
+ const fields = this.table.view?.target?.fields ?? [];
438
215
  if (col < 0 || col >= fields.length) {
439
216
  return undefined;
440
217
  }
441
218
 
442
219
  const field = fields[col];
443
- const dataIndex = this._displayToDataIndex.get(row) ?? row;
220
+ const dataIndex = this._sorting.getDataIndex(row);
444
221
  const value = getValue(this._rows.value[dataIndex], field.path);
445
222
  if (value == null) {
446
223
  return '';
@@ -453,7 +230,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
453
230
  return ''; // TODO(burdon): Show error.
454
231
  }
455
232
 
456
- return getValue(value, field.referencePath);
233
+ return getValue(value.target, field.referencePath);
457
234
  }
458
235
 
459
236
  default: {
@@ -463,8 +240,8 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
463
240
  };
464
241
 
465
242
  public setCellData = ({ col, row }: DxGridPlanePosition, value: any): void => {
466
- const rowIdx = this._displayToDataIndex.get(row) ?? row;
467
- const fields = this.table.view?.fields ?? [];
243
+ const rowIdx = this._sorting.getDataIndex(row);
244
+ const fields = this.table.view?.target?.fields ?? [];
468
245
  if (col < 0 || col >= fields.length) {
469
246
  return;
470
247
  }
@@ -473,7 +250,11 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
473
250
  const { props } = this._projection.getFieldProjection(field.id);
474
251
  switch (props.format) {
475
252
  case FormatEnum.Ref: {
476
- setValue(this._rows.value[rowIdx], field.path, value);
253
+ // TODO(ZaymonFC): This get's called an additional time by the cell editor onBlur, but with the cell editors
254
+ // plain string value. Maybe onBlur should be called with the actual value?
255
+ if (isReactiveObject(value)) {
256
+ setValue(this._rows.value[rowIdx], field.path, makeRef(value));
257
+ }
477
258
  break;
478
259
  }
479
260
 
@@ -492,6 +273,21 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
492
273
  }
493
274
  };
494
275
 
276
+ /**
277
+ * Updates a cell's value using a transform function.
278
+ * @param {DxGridPlanePosition} position - The position of the cell to update.
279
+ * @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
280
+ */
281
+ public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
282
+ const dataRow = this._sorting.getDataIndex(row);
283
+ const fields = this.table.view?.target?.fields ?? [];
284
+ const field = fields[col];
285
+
286
+ const value = getValue(this._rows.value[dataRow], field.path);
287
+ const updatedValue = update(value);
288
+ setValue(this._rows.value[dataRow], field.path, updatedValue);
289
+ }
290
+
495
291
  //
496
292
  // Column Operations
497
293
  //
@@ -501,45 +297,18 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
501
297
  return;
502
298
  }
503
299
 
504
- const field = this.table.view.fields.find((field) => field.id === fieldId);
300
+ const field = this.table.view.target?.fields.find((field) => field.id === fieldId);
505
301
  if (field && this._onDeleteColumn) {
506
- if (this._sorting.value?.fieldId === fieldId) {
507
- this.clearSort();
508
- }
509
302
  this._onDeleteColumn(field.id);
510
303
  }
511
304
  }
512
305
 
513
- //
514
- // Interactions
515
- //
516
- public handleGridClick = (event: React.MouseEvent): void => {
517
- if (!this._modalController.handleClick(event)) {
518
- const target = event.target as HTMLElement;
519
-
520
- const selectionCheckbox = target.closest(`input[${tableControls.checkbox.attributes.checkbox}]`);
521
- if (selectionCheckbox) {
522
- const isHeader = selectionCheckbox.hasAttribute(tableControls.checkbox.attributes.header);
523
- if (isHeader) {
524
- if (this._selection.allRowsSeleted.value) {
525
- this._selection.setSelection('none');
526
- } else {
527
- this._selection.setSelection('all');
528
- }
529
- } else {
530
- const rowIndex = Number(selectionCheckbox.getAttribute(tableControls.checkbox.attributes.checkbox));
531
- this._selection.toggleSelectionForRowIndex(rowIndex);
532
- }
533
- }
534
- }
535
- };
536
-
537
306
  //
538
307
  // Resize
539
308
  //
540
309
 
541
310
  public setColumnWidth(columnIndex: number, width: number): void {
542
- const fields = this.table.view?.fields ?? [];
311
+ const fields = this.table.view?.target?.fields ?? [];
543
312
  if (columnIndex < fields.length) {
544
313
  const newWidth = Math.max(0, width);
545
314
  const field = fields[columnIndex];
@@ -554,11 +323,11 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
554
323
  //
555
324
 
556
325
  public setSort(fieldId: string, direction: SortDirection): void {
557
- this._sorting.value = { fieldId, direction };
326
+ this._sorting.setSort(fieldId, direction);
558
327
  }
559
328
 
560
329
  public clearSort(): void {
561
- this._sorting.value = undefined;
330
+ this._sorting.setSort('', 'asc');
562
331
  }
563
332
 
564
333
  //
@@ -0,0 +1,102 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { describe, expect, it, beforeEach } from 'vitest';
6
+
7
+ import { S, TypedObject } from '@dxos/echo-schema';
8
+ import { create, makeRef } from '@dxos/live-object';
9
+ import { createEchoSchema } from '@dxos/live-object/testing';
10
+ import { createView, ViewProjection } from '@dxos/schema';
11
+
12
+ import { TableModel, type TableModelProps } from './table-model';
13
+ import { TablePresentation } from './table-presentation';
14
+ import { TableType } from '../types';
15
+
16
+ describe('TablePresentation', () => {
17
+ describe('row reactivity', () => {
18
+ let data: any[];
19
+ let updateCount: number;
20
+ let model: any;
21
+ let presentation: TablePresentation;
22
+
23
+ beforeEach(async () => {
24
+ updateCount = 0;
25
+ ({ data } = create({
26
+ data: [
27
+ { col1: 'A', col2: 1, col3: true },
28
+ { col1: 'B', col2: 2, col3: false },
29
+ ],
30
+ }));
31
+
32
+ model = createTableModel({
33
+ onCellUpdate: () => updateCount++,
34
+ });
35
+ model.setRows(data);
36
+ await model.open();
37
+ presentation = new TablePresentation(model);
38
+ });
39
+
40
+ it('should update with row-level reactivity', () => {
41
+ // Set up visible range to include our test data.
42
+ presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
43
+
44
+ // Trigger a row update.
45
+ data[0].col1 = 'New Value';
46
+ expect(updateCount).toBe(1);
47
+
48
+ // Verify the new value through getCells.
49
+ const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 0 } }, 'grid');
50
+ expect(cells['0,0']?.value).toBe('New Value');
51
+ });
52
+
53
+ it('should update reactively when adding a new row', () => {
54
+ // Set up visible range to include our test data.
55
+ presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
56
+
57
+ data.push({ col1: 'C', col2: 3, col3: true });
58
+ expect(updateCount).toBe(1);
59
+
60
+ const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
61
+
62
+ // Verify the new row's data.
63
+ expect(cells['0,2']?.value).toBe('C');
64
+ expect(cells['1,2']?.value).toBe('3');
65
+ expect(cells['2,2']?.value).toBe('true');
66
+ });
67
+
68
+ it('should handle combined operations reactively', () => {
69
+ // Set up visible range.
70
+ presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
71
+
72
+ // Multiple operations.
73
+ data[0].col1 = 'Updated A';
74
+ data.push({ col1: 'C', col2: 3, col3: true });
75
+ data.splice(1, 1);
76
+
77
+ // We expect one update per operation affecting visible rows.
78
+ expect(updateCount).toBe(3);
79
+
80
+ const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
81
+
82
+ // Verify final state.
83
+ expect(cells['0,0']?.value).toBe('Updated A');
84
+ expect(cells['0,1']?.value).toBe('C');
85
+ expect(cells['1,1']?.value).toBe('3');
86
+ expect(cells['2,1']?.value).toBe('true');
87
+ });
88
+ });
89
+ });
90
+
91
+ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
92
+ title: S.String,
93
+ completed: S.Boolean,
94
+ }) {}
95
+
96
+ const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
97
+ const schema = createEchoSchema(Test);
98
+ const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
99
+ const projection = new ViewProjection(schema, view);
100
+ const table = create(TableType, { view: makeRef(view) });
101
+ return new TableModel({ table, projection, ...props });
102
+ };